There's a rite of passage every React developer goes through: you learn about useEffect, and suddenly it's your hammer for everything. Need data? useEffect. Need to sync state? useEffect. Need to do something when a prop changes? useEffect.
Then you ship to production and wonder why your users are seeing spinners for two seconds on a page that should be instant.
The problem with useEffect for data fetching
When you fetch inside useEffect, you're already too late. The component has already rendered once with empty state — meaning the user saw a loading skeleton — before you even started talking to the server. This is called a client waterfall, and it compounds in nested components.
// This pattern has a hidden cost
useEffect(() => {
fetch('/api/posts').then(r => r.json()).then(setPosts)
}, [])With React Server Components, the data fetch happens before the component renders on the client at all. No skeleton, no rehydration mismatch, no extra round-trip. Just HTML with data already baked in.
What I do instead
For anything that can be server-rendered, I just await at the top of an async server component. For truly client-side dynamic data (user-specific, real-time), I reach for SWR or TanStack Query — not raw useEffect.
The mental model shift: the server is not an external thing your UI calls. It's the first place your UI runs.