Why useEffect Is No Longer Recommended for Data Fetching ?

Although useEffect is not being removed from React, it is no longer considered a best practice for data fetching in production applications—especially in the context of Next.js App Router and React Server Components (RSCs). The shift toward server-first data loading fundamentally changes how React apps handle async operations, state, and performance.

The Next.js App Router, powered by React Server Components (RSCs), introduces a paradigm shift. The best practice is to move logic and data fetching to the server, which minimizes client-side JavaScript and fundamentally changes how we manage state and side effects.

The Core Issue: Performance & Network Waterfalls

Fetching data in useEffect forces data loading to happen after the component renders. This creates what’s known as a network waterfall, where each step waits for the previous one:

  1. Component renders
  2. useEffect runs
  3. Network request begins
  4. Data arrives
  5. Component re-renders

This delays when users see real content and prevents the browser from fetching data in parallel while JavaScript loads. Example of the Problem

export default function Dashboard() {
  const [data, setData] = useState(null);

  // ❌ Causes a network waterfall
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(setData);
  }, []);

  return <div>{/* render data */}</div>;
}

Why This Approach Is Inefficient

  • Sequential loading: The browser cannot start the request until after the initial render.
  • Slower perceived performance: Users see a loading state instead of actual content.
  • No parallelization: Fetching can’t start during server rendering or during bundle download.
  • Nested waterfalls: Parent → Child → Grandchild fetch chains stack delays dramatically.

This behavior is one of the most common causes of slow page loads in client-heavy React apps.


The Modern Approach (2025): Server-First Data Fetching

With the Next.js App Router and React 19 features, data fetching is intended to happen on the server, not the client. The ecosystem now offers tools designed for predictable, parallel, and high-performance async workflows:

  • React Server Components (RSCs)
  • Server Actions
  • React’s new async hooks (use, useTransition, useOptimistic)
  • Streaming and Suspense-first architecture

These tools allow React to begin fetching data before the page reaches the browser and to stream UI in chunks without waiting for all data.


1. Best: Server-Side Fetching in an Async Server Component

This is the preferred pattern for initial page loads. It minimizes client JavaScript, enables automatic parallelization, and works natively with Suspense and streaming. We will demonstrate an example on it.

2. Good: Client-Side Server State Libraries

Tools like React Query or SWR remain valuable for client-driven interactions (e.g., user-triggered mutations, infinite scroll).

// Using TanStack Query
import { useQuery } from '@tanstack/react-query';

function Dashboard() {
  const { data, isLoading } = useQuery({
    queryKey: ['data'],
    queryFn: () => fetch('/api/data').then(res => res.json())
  });

  // Better caching, background updates, error states
}

3. Acceptable (Framework-Specific): Loaders in Other Frameworks

Patterns like Remix loaders or server loaders in router-based frameworks work well but don’t apply directly to Next.js. Acceptable (But Not Directly Applicable to Next.js): Framework Loaders.

// With React Router loaders
export async function loader() {
  const response = await fetch(`/api/data`);
  return response.json();
}

// ✅ Data fetches in parallel with code loading
export default function Dashboard({ loaderData }) {
  return <div>{loaderData.map(item => <div key={item.id}>{item.name}</div>)}</div>;
}

4. Least Recommended: Raw useEffect + useState for Data Fetching ❌

This is the legacy pattern and should be avoided for initial page loads or critical content.


Example: Server-Side Fetching in an Async Server Component

In the Next.js App Router, the recommended way to fetch data for the initial page load is directly inside an async Server Component. This allows React to fetch data on the server before sending HTML to the user, eliminating client-side waterfalls and improving performance.

How It Works

Initial Render (Next.js Server):
The Next.js server handles the first request and performs data fetching during the server render. Because the work happens on the server:

  • Fetches run before HTML is sent to the browser
  • Data can be cached and revalidated
  • Content is SEO-friendly and indexable
  • No client JavaScript is required to display the initial data

This model uses React Server Components (RSCs), which allow you to run async data fetching directly in the component body—something not possible in Client Components. The In our app/products/page.ts file

import Link from "next/link";
import { Product } from "@/types/product";
import DeleteProductButton from "./components/DeleteProductButton";

async function getProducts(): Promise<Product[]> {
  const res = await fetch("http://localhost:8000/products", {
    // Incremental Static Regeneration (ISR): revalidate every 60 seconds
    next: { revalidate: 60 },
  });

  if (!res.ok) {
    throw new Error("Failed to fetch products");
  }
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();

  return (
    <>
      <div className="pt-3 pb-2 mb-3 border-b border-gray-200 flex justify-end">
        <Link
          href="/products/create"
          className="inline-flex items-center px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
        >
          Add
        </Link>
      </div>

      <div className="overflow-x-auto">
        <table className="min-w-full divide-y divide-gray-200">
          <thead className="bg-gray-50">
            <tr>
              {["#", "Name", "Price", "Quantity", "Actions"].map((header) => (
                <th
                  key={header}
                  scope="col"
                  className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
                >
                  {header}
                </th>
              ))}
            </tr>
          </thead>

          <tbody className="bg-white divide-y divide-gray-200">
            {products?.map((product) => (
              <tr key={product.id}>
                <td className="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
                  {product.id}
                </td>
                <td className="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
                  {product.name}
                </td>
                <td className="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
                  ${product.price}
                </td>
                <td className="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
                  {product.quantity}
                </td>
                <td className="px-3 py-2 whitespace-nowrap text-sm font-medium">
                  <DeleteProductButton
                    productId={product.id!}
                    productName={product.name}
                  />
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </>
  );
}

✅ What next: { revalidate: 60 } Means

1. Cache the response for 60 seconds

When Next.js fetches this URL on the server, it stores the result in its cache.

  • For 60 seconds, all visitors get the cached response.
  • No new network request is made during this period.

2. After 60 seconds, re-fetch the data (Incremental Static Regeneration)

Once the 60-second window expires, the next incoming request triggers:

  • A background re-fetch of the data
  • The cache updates with the fresh response

This process is called Incremental Static Regeneration (ISR).

3. Users still get fast responses

Even during revalidation:

  • The current cached data is served immediately
  • The update happens in the background
  • The page stays fast and responsive

Why This Approach Is Better

🚀 No useEffect Needed

  • The fetch runs during the server render, not in the browser.
  • No waterfalls, no client-state management, no loading spinners for initial data.

Better Performance

  • Data fetching happens in parallel with JavaScript bundling.
  • React can stream the UI using Suspense boundaries.
  • Next.js applies automatic caching and ISR-based revalidation.
  • Faster Time to First Byte (TTFB) and First Contentful Paint (FCP).

🔍 SEO Optimized

  • Because HTML is pre-rendered with real data, crawlers receive full content.
  • Great for products, blogs, marketing pages, listings, etc.

🎨 Better User Experience

  • Users see fully populated content immediately.
  • No flash of loading states or skeletons for the initial render.

🧩 Framework-Level Benefits

  • Automatic code splitting based on the file tree.
  • Built-in request memoization and caching.
  • Easy to configure revalidation for near-real-time data freshness.
  • Works seamlessly with Server Actions and future React async features.

🔹 React Query (TanStack Query) and Next.js — How They Fit Together

Next.js does not use React Query internally for its built-in data fetching system. Server Components, route handlers, and the native fetch() caching system are all handled directly by Next.js and React — not by React Query.

However, in real production apps, you often need client-side data management that Server Components cannot handle (e.g., forms, user-triggered actions, real-time updates, pagination, mutations). For these cases, React Query is the industry-standard library.

Here’s the simple breakdown:

Next.js Core Data Fetching

  • Uses built-in caching, revalidation, and Server Components
  • No React Query involved
  • Best for initial page loads, static data, SEO pages, and server-side rendering

Next.js Client-Side Data Fetching (Production Apps)

  • React Query (or SWR) is the recommended tool
  • Handles client-only data, background syncing, optimistic updates, mutations, and caching
  • Used inside Client Components ("use client")

Next.js handles server-side data. React Query handles client-side data. Together, they cover both halves of a modern full-stack React application


✅ Why useEffect Is Still Important (Even If It’s Not for Data Fetching)

There’s a misconception that useEffect is “deprecated” or “obsolete.” This is not true. React is simply encouraging developers to stop using useEffect for initial data fetching, because Server Components now do that faster and more efficiently.

But the original purpose of useEffect is still 100% valid:

useEffect synchronizes your component with things outside of React.

In other words:
useEffect runs when your component needs to react to something in the browser, in another system, or in an external API that React cannot automatically track.

Server Components cannot handle these tasks, because they run on the server, not in the browser.

That means: 👉 There are still many cases where useEffect is the correct and only tool.


❌ When useEffect Is NOT Needed Anymore

❌ 1. Initial Data Fetching: Use Server Components, not useEffect.

Why?

  • Faster
  • Cached
  • No waterfalls
  • Runs before HTML is sent
  • No loading spinners needed

Example: Fetching products, posts, dashboards, settings, etc. → Should be server-side


✅ When useEffect IS Still Necessary

Here is the simplified list with explanations.

1. Client-Side Interactivity

Example: controlling a modal, drawer, or dropdown.

const [open, setOpen] = useState(false);

This does not need useEffect, but if you need to react to the state change (e.g., lock scroll, track open state), you would use useEffect.

2. Subscriptions or Event Listeners

These things exist in the browser, not on the server — so Server Components cannot do them. Examples where useEffect is required:

  • WebSocket connections
  • Listening to window.resize
  • Listening to scroll events
  • Third-party library events
  • Connecting to a real-time service (Pusher, Firebase, Ably)

React has no built-in way to manage external event streams — so useEffect steps in.

3. Browser APIs

Server Components cannot access browser-only objects. Examples requiring useEffect:

  • localStorage
  • sessionStorage
  • window
  • document
  • navigator
  • Intersection Observer
  • Clipboard API

You need useEffect because these APIs don’t exist during server rendering.

// ✅ Correct use of useEffect for an external browser API (must be in a 'use client' file)
useEffect(() => {
  const handleScroll = () => { /* ... logic ... */ };
  window.addEventListener('scroll', handleScroll);

  return () => { // 👈 Crucial Cleanup
    window.removeEventListener('scroll', handleScroll);
  };
}, []);

4. Cleanup Logic

If you start something, you must stop it — and React’s cleanup system lives inside useEffect. Examples:

  • Clearing intervals or timeouts
  • Closing WebSockets
  • Detaching observers
  • Unsubscribing from listeners

This is one of the main reasons useEffect exists.

5. Client-Side Data Mutations

For actual data submissions, use Server Actions. But sometimes you still handle client behavior after a mutation.

Example:

  • Show a toast
  • Redirect after submit
  • Optimistic UI updates
  • Track analytics

A Server Action performs the mutation, but useEffect can respond to the result on the client.

6. Complex Form Logic

React Hook Form, Formik, and other libraries still use useEffect internally.

You generally don’t write useEffect yourself here — but form libraries depend on it to:

  • Watch field changes
  • Register inputs
  • Validate rules
  • Track dirty state

Server Components cannot replace this.

7. Debouncing, Throttling, or Delayed Operations

For example:

  • Searching only after user stops typing
  • Delayed animations
  • Auto-saving drafts

Sometimes URL search params replace this, but not always.

useEffect is still the natural tool for timing-based behavior.


🎯 Simple Summary (Beginner-Friendly)

❌ Don’t use useEffect for:

  • Fetching data for the initial page load

✅ Do use useEffect for:

  • Anything involving the browser
  • Anything involving side effects
  • Anything that needs cleanup
  • Anything that depends on external real-time systems

Summary : Server Components handle data. Client Components handle interactions and effects.

Related Articles

  1. Mastering generateStaticParams() in Next.js 14: Boost Performance and SEO
  2. Understanding Nextjs Server Actions and Mutations 2024
  3. Understanding Routing in Next.js
  4. Understanding Prefetching in Next.js
  5. Data Fetching and Caching in Next.js 

Scroll to Top