DesignDev.io logoDesignDev.io logo

  1. Home
  2. /React Foundations
  3. /Why I Stopped Using useEffect for Data Fetching (And What I Use Instead)
← Back to home

Why I Stopped Using useEffect for Data Fetching (And What I Use Instead)

Alex Chen

Alex Chen

Cover Image for Why I Stopped Using useEffect for Data Fetching (And What I Use Instead)
Alex Chen

Alex Chen

January 1, 2026·4 min read
Updated April 26, 2026
Series:React Foundations
Level:Intermediate
Tags:reactweb
Assumes:Basic React knowledge, familiarity with hooks

useEffect data fetching is the pattern every React tutorial teaches and almost no production codebase should use. Here's what the race conditions, missing cache, and waterfall requests look like in practice — and the tools that solve them properly


If you've been writing React for more than six months, you've written this code. Probably more than once.

It works. It's familiar. It's in every tutorial from 2019 to 2023. It's also wrong for most use cases — and I'll show you exactly why.

The problems nobody talks about

Race conditions

Here's the bug you've probably shipped without knowing it. A user navigates to a profile page. The fetch starts. They click to a different profile before the first fetch completes. Now two fetches are in flight. Whichever one resolves last wins — even if it's the one for the profile they left.

The not so good fix for race conditions problem — an isCancelled flag or AbortController — adds additional ten lines and is easy to get wrong.

This is now twelve lines to do what should be only two. And most developers don't write it this way anyway.

No deduplication

If three components on the same page each render useEffect(() => fetch('/api/config'), []), you get three network requests for the same data. There's no coordination between component instances.

No caching

Every mount triggers a new fetch. Navigate away, come back — another fetch. There's no built-in way to say "if we fetched this data in the last 60 seconds, use it."

Waterfalls

When a parent component fetches and passes data to a child that also fetches, you've created a request waterfall. The second request can't start until the first resolves. useEffect makes waterfalls the default outcome rather than the exception.


What TanStack Query solves

TanStack Query (formerly React Query) is not "just a caching library." It's a server state manager — and server state is genuinely different from client state.

Client state: which modal is open, what the user has typed in an input field.
Server state: data that lives on a server, is shared across users, and can become stale.

Here's the same data fetch with TanStack Query:

Six lines. No useState. No useEffect. No cleanup function.

What you get automatically:

Deduplication. Three components that call useQuery({ queryKey: ['user', '1'] }) trigger one network request. The other two wait for the first to resolve.

Caching. The result is cached by queryKey. Navigate away, come back — the cached data shows instantly while a background refetch runs silently.

Race condition handling. Handled internally. You never write an isCancelled flag again.

Background refetching. When a user returns to a browser tab, stale data is refetched automatically.

Loading and error states. isLoading, isFetching, isError, error — all derived from the query state without managing it yourself.


What about React's use() hook?

If you're working in Next.js App Router or any setup that uses React Server Components, the answer looks different again.

In a Server Component, you don't need useEffect or TanStack Query at all:

You're awaiting the data directly. No hook, no loading state, no effect. The Suspense boundary above it handles the loading state.

For data that starts on the server but needs to be read in a Client Component, React 19's use() hook lets you pass a Promise as a prop and read it:

This is powerful but requires a Suspense boundary. It's the right tool when you want Server Component data fetching but need client interactivity on the result.


When should you still use useEffect for fetching?

Almost never for primary data loading. But there are legitimate edge cases:

Imperative integrations. Third-party SDKs that don't expose a Promise-based API and require you to register callbacks.

Subscriptions. WebSocket connections, EventSource streams, or real-time subscriptions where data arrives continuously rather than once.

Side effect triggered by user action. An analytics event or logging call that fires when something renders — though these are usually better placed in event handlers than effects.

If you find yourself writing useEffect(() => { fetch(...) }, []) for standard CRUD data loading in a component that doesn't need RSC, that's the pattern to replace.


The decision rule

Here's how I think about it:

  • Next.js App Router, Server Components available? → Fetch directly in the Server Component with async/await
  • Client Component that needs server data? → TanStack Query with useQuery
  • Real-time data that updates continuously? → useEffect with proper cleanup and a WebSocket or SSE connection
  • Third-party SDK that requires useEffect? → Use it, write the cleanup function, move on

The goal isn't to never use useEffect. It's to not use it as a fetch wrapper when better tools exist specifically for that job.


Migrating existing code

If you have a codebase full of useEffect data fetches, you don't need to rewrite everything at once. TanStack Query is additive — you can migrate one component at a time.

  1. Install: pnpm add @tanstack/react-query
  2. Wrap your app in QueryClientProvider
  3. Replace one useEffect fetch with useQuery
  4. Delete the three useState calls that went with it

The QueryClient handles caching globally, so the moment you migrate a component, it benefits from deduplication with any other component that uses the same queryKey.


The useEffect data fetch pattern exists because it worked before we had better options. TanStack Query has been production-ready since 2020. The App Router has been stable since 2023. The better options exist. Use them.


Up next: How to build a custom hook that actually earns its abstraction — the patterns that make custom hooks worth writing.

Related: React Server Components explained without the hype — the mental model that makes RSC click.

Alex Chen is a senior frontend engineer who writes about React patterns, JavaScript internals, and the decisions that separate maintainable codebases from ones that fight back. Opinionated by design.

Photo by Pankaj Patel on Unsplash


More Stuff

Building a Badge Component — Variants, Sizes, and Dot Indicators
April 3, 2026·3 min read

Building a Badge Component — Variants, Sizes, and Dot Indicators

Badge is the first purely presentational primitive in this series — no form integration, no event handling, no focus management. That simplicity is deceptive. A Badge that looks right in isolation often breaks inside a nav item, a table cell, or an avatar. The decisions that make it composable are worth getting right now.

Muhammad Athar

Muhammad Athar

Building a Label Component That Actually Connects to Its Field
April 3, 2026·3 min read

Building a Label Component That Actually Connects to Its Field

We've referenced a Label component in every form field article so far without building it. That's intentional — you need to understand what the Label connects to before you understand what it needs to do. Now that we have Input, Textarea, Checkbox, RadioGroup, Select, and Toggle, the Label's job is clear: it's not just styled text. It's the accessible name of a form field, and it carries required indicators, optional markers, and disabled state as part of that responsibility.

Muhammad Athar

Muhammad Athar

DesignDev.io logoDesignDev.io logo

Explore

  • Home
  • Search
  • Authors
  • Series
  • About Us

Account

  • Sign in

Legal

  • Privacy Policy
  • Terms of Service

© 2026 DesignDev.io · All rights reserved

Built with Next.js · Tailwind · Sanity · Vercel