09-nextjsTermsLevel_02React useEffect Hook

React useEffect Hook

Level 2 — App Router UI Elements A hook that allows functional Client Components to perform side effects that synchronize React state with external non-React systems.


1. Prerequisites

  • React Hooks — The system governing hook executions.

2. Term Category

React Server Component (Client Side Effect Hook): useEffect() manages client-side DOM mutations, timer intervals, and external event subscriptions inside Client Components.


3. Explanation

Environment Context

  • Client Only (Side effects run after rendering inside the client's browser DOM).

(1) Design Motivation — "Why did we design this?"

React components should ideally be pure functions that accept props, read state, and return JSX. However, real-world applications need to interact with external services and APIs that are not managed by React's rendering pipeline. Examples include:

  • Fetching data from a backend server.
  • Directly updating the browser document title.
  • Setting up intervals or timeouts.
  • Adding event listeners directly to the global window object.

useEffect was designed to solve this by providing a controlled entry point to run side effects. It schedules the effect code to run asynchronously after the component has rendered and the browser has painted the UI, ensuring that slow actions do not block the initial page paint.


(2) Core Concept — Dependencies and Cleanup

The hook takes two arguments: the side effect function, and an optional array of dependencies:

'use client';

import React, { useState, useEffect } from 'react';

export default function DocumentTracker() {
  const [clicks, setClicks] = useState<number>(0);

  // Synchronizes document title with local state
  useEffect(() => {
    document.title = `Total Clicks: ${clicks}`;
    
    // Optional: Dependency array controls when the effect executes
  }, [clicks]); // Only re-run when clicks state changes

  return (
    <button onClick={() => setClicks(clicks + 1)}>
      Increment Count
    </button>
  );
}

(3) Effect Cleanups (Avoiding Memory Leaks)

Many side effects create persistent subscriptions or listeners that continue executing even after the component is unmounted (removed from the page). To prevent memory leaks, you must return a Cleanup Function from your effect block:

'use client';

import React, { useEffect } from 'react';

export default function ScrollListener() {
  useEffect(() => {
    const handleScroll = () => console.log(window.scrollY);
    
    // 1. Establish subscription
    window.addEventListener('scroll', handleScroll);

    // 2. Return cleanup callback to tear down subscription
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []); // Empty array means run once on mount, clean up on unmount

  return <p>Scroll down to see coordinates in console.</p>;
}

4. Common Mistakes & Pitfalls

Mistake 1: Setting state unconditionally inside useEffect without dependencies

The mistake: Triggering a state update inside an effect without specifying dependencies, or missing dependencies:

// BAD: Triggers an infinite rendering loop!
export default function InfiniteLoop() {
  const [data, setData] = useState(0);

  useEffect(() => {
    // State update triggers re-render, which triggers effect, which triggers state update...
    setData(data + 1); 
  }); 

  return <div>Count: {data}</div>;
}

Why it's wrong: Omitting the dependency array causes useEffect to execute on every single render. Since setting state triggers a re-render, the component gets trapped in an infinite rendering loop, eventually crashing the user's browser.

Golden Rule: Always declare a dependency array when calling useEffect, and verify that all state or prop variables referenced inside the effect are listed inside that array.


Mistake 2: Using useEffect for Primary Data Fetching in Next.js App Router (Waterfall Trap)

The mistake: Fetching page data inside useEffect in a Client Component.

Why it's wrong: Fetching data in useEffect requires downloading JS bundles first, causing slow waterfalls, layout shifts, and poor SEO. Fetch data directly in async React Server Components.

Incorrect:

'use client';
export default function Page() {
  const [data, setData] = useState(null);
  useEffect(() => { fetch('/api/user').then(r => r.json()).then(setData); }, []); // ❌ CSR Waterfall!
}

Fix:

// Async Server Component fetches data on server directly:
export default async function Page() {
  const res = await fetch('https://api.example.com/user');
  const data = await res.json();
  return <div>{data.name}</div>;
}

Mistake 3: Forgetting Event Listener Cleanup Functions in useEffect

The mistake: Adding window.addEventListener('scroll', handleScroll) inside useEffect without returning a cleanup function.

Why it's wrong: Un-cleared window listeners remain registered after component unmounting, causing memory leaks and state updates on unmounted components.

Incorrect:

useEffect(() => {
  window.addEventListener('resize', handleResize); // ❌ Missing cleanup return function!
}, []);

Fix:

useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize); // Cleanup
}, []);

5. Practice Exercises

Exercise 1: Managing Client Side Effects with useEffect()

Scenario: Subscribe to browser window resize events using useEffect() and clean up the listener on unmount.

Requirements:

  1. Add event listener in useEffect() and return cleanup function.
Answer

Implementation

"use client";

import { useState, useEffect } from "react";

export default function WindowSize() {
  const [width, setWidth] = useState<number>(0);

  useEffect(() => {
    // Client-side window access
    setWidth(window.innerWidth);
    
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);

    // Cleanup listener on component unmount
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return <p>Window Width: {width}px</p>;
}

Technical Explanation

  1. useEffect(callback, dependencies) executes side effects after component rendering.
  2. Returning a function from the callback performs cleanup when the component unmounts or dependencies update.
  3. Empty dependency array [] runs the effect once on mount.

Exercise 2: Fetching Data in Client Components with AbortController

Scenario: Implement client-side data fetching in useEffect() with AbortController cleanup to cancel stale network requests.

Requirements:

  1. Pass signal to fetch() and abort on cleanup.
Answer

Implementation

"use client";

import { useState, useEffect } from "react";

export default function ClientSearch({ query }: { query: string }) {
  const [results, setResults] = useState<any[]>([]);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then((res) => res.json())
      .then((data) => setResults(data))
      .catch((err) => {
        if (err.name !== "AbortError") console.error(err);
      });

    return () => controller.abort();
  }, [query]);

  return <ul>{results.map((r, i) => <li key={i}>{r.name}</li>)}</ul>;
}

Technical Explanation

  1. controller.abort() cancels ongoing HTTP fetch requests when query prop updates rapidly.
  2. Prevents race conditions where old responses overwrite newer search results.
  3. Robust client-side side effect pattern.

Exercise 3: Avoiding useEffect() for Server Component Migration

Scenario: Refactor a Client Component using useEffect() for initial data fetching into a zero-bundle-size async Server Component.

Requirements:

  1. Replace useEffect() data fetching with direct await fetch() in Server Component.
Answer

Implementation

// ❌ OLD CLIENT APPROACH:
// useEffect(() => { fetch('/api/user').then(...) }, []);

// ✅ NEW RSC APPROACH (app/user/page.tsx):
export default async function UserPage() {
  const res = await fetch("https://api.example.com/user");
  const user = await res.json();

  return <div>User: {user.name}</div>;
}

Technical Explanation

  1. In Next.js App Router, prefer Server Components over useEffect() for initial data fetching.
  2. Eliminates client-side loading spinners and waterfall network roundtrips.
  3. Reduces client JavaScript bundle size to zero bytes for data fetching.

  • React Hooks — The parent hook mechanism.
  • template.tsx — The Next.js UI file that intentionally re-triggers effects on page navigations.

7. Key Takeaways

  • useEffect coordinates side effects that sync React state with external systems.
  • Effects execute asynchronously after rendering and paint are completed.
  • Use the dependency array to limit execution to when specific variables change.
  • An empty dependency array [] ensures the effect runs only once on component mount.
  • Return a cleanup function from your effect to tear down listeners, intervals, and subscriptions.
Built with LogoFlowershow