Skip to main content
Juliano Alves
Back to blog

Less useEffect: data fetching in modern React

5 min read
By Juliano Alves

For years, tutorials taught: “on mount, useEffect + fetch”. That pattern is easy to write and hard to get right at scale. Modern React and frameworks give you better primitives—if you match the tool to where data actually lives.

What goes wrong with fetch-in-effect#

Timing and double invocation#

useEffect runs after paint. Users see empty UI, then a flash of loading, then content. In React 18 Strict Mode (development), effects that set up subscriptions or fetch run twice on purpose to surface unsafe side effects. If your effect is not idempotent, you get duplicate requests or race conditions.

Dependency arrays#

Omitting dependencies “fixes” stale data bugs by lying to the linter. Including the wrong dependencies causes infinite loops or redundant refetches. useEffect conflates synchronization with data loading, which are different concerns.

Waterfalls#

Parent effect runs, then child mounts, then child effect runs. Without careful orchestration you build request waterfalls—each layer waits for the previous—where parallel fetching would have been faster.

Decision matrix (simplified)#

Constraint Prefer
Data needed for first paint SEO / fast TTFB Server Component or route loader
Shared cache, retries, background refresh on client TanStack Query (or similar)
User gesture triggers one-off fetch Event handler (onSubmit, onClick)
Subscribing to browser API or external stream useEffect with cleanup

Server Components (Next.js App Router)#

If the tree can run on the server, async components and fetch with explicit caching replace a whole class of client effects:

export default async function Page() {
  const res = await fetch('https://cms.example.com/page/home', {
    next: { revalidate: 300 },
  });
  const data = await res.json();
  return <HomeView data={data} />;
}

No useState for initial data, no “hydration mismatch” from missing skeleton discipline if you keep the async boundary predictable.

Client libraries: TanStack Query#

For data that changes after mount, needs deduplication across components, or should refetch on focus/reconnect, client libraries encode the state machine:

  • isLoading / isFetching / error / data
  • queryKey for cache identity
  • staleTime to avoid hammering the API
const { data, error, isPending } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetch(`/api/users/${userId}`).then((r) => r.json()),
  staleTime: 60_000,
});

This replaces hand-rolled useEffect, useState, and useReducer for the common CRUD read path.

Event-driven fetch#

Search-as-you-type, infinite scroll “load more”, and form submission should usually not use useEffect as the trigger. Tie fetches to the event that caused them; debounce in the handler or with a small helper.

When useEffect is still correct#

Effects are for synchronizing React with an external system:

  • window.addEventListener('resize', …) with removal on cleanup.
  • document.title updates based on props.
  • WebSocket subscription with socket.close() in cleanup.
  • Imperative third-party widgets (maps, charts) that need DOM nodes.

For these, keep dependencies minimal and document why the effect exists.

React 19 and use#

React 19 introduces use for reading promises and context in render (with Suspense). It does not remove the need for good data architecture, but it further blurs the line between “data in render” and effects. If you adopt it, ensure boundaries and error handling match your router’s streaming model.

Migration strategy for legacy code#

  1. List every useEffect that calls fetch.
  2. For each, classify: initial page data, user-triggered, or live subscription.
  3. Move initial data to the server where possible.
  4. Wrap remaining reads in Query with identical URLs and keys.
  5. Leave true side effects in useEffect with tests for cleanup.

Summary#

useEffect is a lifecycle hook, not a data layer. Server rendering, dedicated client cache libraries, and event handlers cover most fetching needs with clearer semantics and fewer race conditions. Reserve useEffect for real side effects—and your future self will thank you in code review.

© 2026 Juliano Alves. All rights reserved.