← All posts

Why I Stopped Using useEffect for Data Fetching

React 19 and the RSC model changed how I think about async data — and killed most of my useEffect calls overnight.

Jul 25, 20261 min read201 words

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.

reactnextjsperformance