Useful Cheatsheetsusefulcheatsheets.com
React useEffect Hook
Chapter 23 · Page 161
Intermediate

React useEffect Hook

Handle side effects, manage dependencies, and clean up after effects.

TL;DR

  1. 01useEffect runs code after render for side effects like fetching data.
  2. 02Dependency array controls when the effect runs.
  3. 03Return a cleanup function to prevent memory leaks.

Tips

  1. 01Always include all dependencies in the dependency array — ESLint's exhaustive-deps rule helps catch missing ones.

Warnings

  1. 01Forgetting cleanup functions causes memory leaks — always clean up subscriptions, timers, and listeners.
Notes
Useful Cheatsheetsusefulcheatsheets.com
React useEffect Hook
Chapter 23 · Page 162
Intermediate

React useEffect Hook

(continued)

Basic useEffect

  • Run a side effect after every render by calling useEffect with no dependency array.

    useEffect(() => {
      console.log("Component rendered"); // runs after every render
    });
    
  • Update the document title after each render as a common side-effect pattern.

    useEffect(() => {
      document.title = `You clicked ${count} times`;
      // Runs after every render where count may have changed
    });
    
  • Wrap async logic in an inner function because the effect callback itself cannot be async.

    useEffect(() => {
      async function loadData() {
        const res = await fetch("/api/users");
        const data = await res.json();
        setUsers(data);
      }
      loadData(); // call the async function immediately
    }, []);
    
  • Use useEffect for imperative side effects like logging, analytics, or third-party integrations.

    useEffect(() => {
      analytics.track("page_view", { page: pathname }); // fire-and-forget side effect
    }, [pathname]); // re-fires when the route changes
    
  • Keep effect callbacks synchronous at the top level and wrap any async work inside.

    // Wrong: async effect callback — cleanup return value is lost
    useEffect(async () => { await doSomething(); }, []);
    
    // Right: inner async function with explicit call
    useEffect(() => {
      async function run() { await doSomething(); }
      run();
    }, []);
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React useEffect Hook
Chapter 23 · Page 163
Intermediate

React useEffect Hook

(continued)

Dependency Comparison Rules

  • Know that React compares each dependency to its previous value with Object.is, not a deep equality check — this is the exact mechanism that decides whether an effect re-runs.

    useEffect(() => {
      console.log("Count changed:", count);
      // Object.is(prevCount, count) === false triggers a re-run
    }, [count]);
    
  • Expect object and array literals to fail Object.is every render because a new reference is created each time, even when the contents are identical.

    useEffect(() => {
      fetchUserData(filters); // filters = { status: "active" } looks "changed" every render
    }, [filters]); // new object reference each render → effect re-fires constantly
    
  • Fix reference-instability by memoizing the object/array with useMemo or by destructuring to primitive dependencies instead.

    useEffect(() => {
      fetchUserData(status, token); // primitives compare correctly with Object.is
    }, [status, token]); // both must appear in the deps array
    
  • Omit the array entirely only when the effect must run after every single render — each item is still diffed with Object.is, there's just no array to skip the check.

    useEffect(() => {
      // Runs after EVERY render — use only when you truly need this
      syncStateToExternalSystem(value);
    }); // no array = every render
    
  • Enable the ESLint react-hooks/exhaustive-deps rule so it statically flags any variable read inside the effect that's missing from the array — it does not understand Object.is semantics, only static usage.

    # Install the rules-of-hooks plugin
    npm install eslint-plugin-react-hooks --save-dev
    # Add to .eslintrc: "react-hooks/exhaustive-deps": "warn"
    # Use the rule's quick-fix to auto-insert missing deps, then verify literals are memoized
    
  • Avoid silencing exhaustive-deps with an inline disable comment — it almost always hides a stale-closure bug rather than a false positive.

    useEffect(() => {
      fetchUserData(userId); // eslint-disable-next-line react-hooks/exhaustive-deps
      // Tempting but dangerous: userId changes won't re-trigger this effect
    }, []);
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React useEffect Hook
Chapter 23 · Page 164
Intermediate

React useEffect Hook

(continued)

Data Fetching

  • Fetch data when the component mounts by using an empty dependency array.

    useEffect(() => {
      fetch("/api/data")
        .then((r) => r.json())
        .then(setData);
    }, []); // runs once on mount
    
  • Track loading and error state alongside the fetch for complete async UI handling.

    useEffect(() => {
      setLoading(true);
      fetch(url)
        .then((r) => r.json())
        .then(setData)
        .catch(setError)
        .finally(() => setLoading(false));
    }, [url]); // re-fetches when url changes
    
  • Cancel stale requests with AbortController to prevent setting state after unmount.

    useEffect(() => {
      const controller = new AbortController();
      fetch(url, { signal: controller.signal })
        .then((r) => r.json())
        .then(setData)
        .catch((err) => {
          if (err.name !== "AbortError") setError(err); // ignore intentional cancellation
        });
      return () => controller.abort(); // cancel on unmount or url change
    }, [url]);
    
  • Re-fetch automatically when a dependency like userId changes by including it in the array.

    useEffect(() => {
      async function load() {
        const res = await fetch(`/api/users/${userId}`);
        setUser(await res.json());
      }
      load();
    }, [userId]); // re-runs every time userId changes
    
  • Use SWR or React Query in production for caching, deduplication, and automatic revalidation.

    import useSWR from "swr";
    
    // SWR handles loading state, caching, and revalidation on focus
    const { data, error, isLoading } = useSWR("/api/user", fetcher);
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React useEffect Hook
Chapter 23 · Page 165
Intermediate

React useEffect Hook

(continued)

Cleanup Functions

  • Return a function from the effect callback itself — this is the only way React knows it's a cleanup function, and it fires before unmount or before the next effect run.

    useEffect(() => {
      const subscription = subscribe((data) => {
        setData(data);
      });
      return () => subscription.unsubscribe(); // cleanup before unmount
    }, []);
    
  • Remember that the cleanup function closes over the props/state values from the render that scheduled it, not the latest render — it always sees the OLD values, never the current ones.

    useEffect(() => {
      const id = roomId; // captured by this closure
      connect(id);
      return () => disconnect(id); // disconnects the OLD roomId, even after roomId changes
    }, [roomId]);
    
  • Picture the exact ordering on every dependency change: React runs the PREVIOUS effect's cleanup first, fully, before invoking the NEW effect's setup — never interleaved.

    useEffect(() => {
      const timer = setInterval(() => setTime((t) => t + 1), 1000);
      return () => clearInterval(timer); // old timer fully cleared before a new one starts
    }, [intervalMs]); // change in intervalMs: cleanup(old) → setup(new), in that order
    
  • Pair every subscription-style API call with its exact inverse in cleanup — addEventListener/removeEventListener, connect/disconnect, open/close — so each effect run leaves zero residue for the next one to build on.

    useEffect(() => {
      const handleResize = () => setWidth(window.innerWidth);
      window.addEventListener("resize", handleResize);
      return () => window.removeEventListener("resize", handleResize); // exact inverse of setup
    }, []);
    
  • Treat a missing cleanup return as a silent bug, not a no-op — React still re-runs the new effect on every dependency change, it just never undoes the old one first.

    useEffect(() => {
      const ws = new WebSocket(`wss://example.com/${roomId}`);
      ws.onmessage = (e) => setMessages((prev) => [...prev, e.data]);
      return () => ws.close(); // without this, every roomId change leaks an open socket
    }, [roomId]);
    
  • Re-subscribe correctly when a dependency like userId changes by relying on cleanup to unsubscribe from the PREVIOUS value before the new effect subscribes to the current one.

    useEffect(() => {
      const sub = subscribe(userId); // subscribe to new user
      return () => sub.unsubscribe(); // unsubscribe from PREVIOUS user first
    }, [userId]); // runs cleanup on every userId change before re-subscribing
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React useEffect Hook
Chapter 23 · Page 166
Intermediate

React useEffect Hook

(continued)

useEffect Patterns

  • Sync state with localStorage by reading on mount and writing when state changes.

    useEffect(() => {
      const saved = localStorage.getItem("theme");
      if (saved) setTheme(saved); // restore on mount
    }, []);
    
    useEffect(() => {
      localStorage.setItem("theme", theme); // persist on every change
    }, [theme]);
    
  • Track the previous value of a variable by storing it in a ref before each update.

    const prevCountRef = useRef<number | undefined>(undefined);
    
    useEffect(() => {
      prevCountRef.current = count; // store current as previous after each render
    }, [count]);
    
    const prevCount = prevCountRef.current; // previous value available during render
    
  • Use multiple separate useEffect calls for unrelated concerns to keep each effect focused.

    useEffect(() => { /* handle auth token refresh */ }, [token]);
    useEffect(() => { /* sync theme to body class */ }, [theme]);
    useEffect(() => { /* track window resize */ }, []);
    // Three concerns → three effects, each independently managed
    
  • Focus an input or dialog element after it becomes visible in the DOM.

    useEffect(() => {
      if (isOpen && inputRef.current) {
        inputRef.current.focus(); // focus runs after the DOM updates
      }
    }, [isOpen]); // re-runs whenever isOpen toggles to true
    
  • Update the document title reactively to reflect dynamic values like unread counts.

    useEffect(() => {
      document.title = unreadCount > 0
        ? `(${unreadCount}) My App`
        : "My App";
    }, [unreadCount]); // updates whenever unreadCount changes
    
  • Treat Strict Mode's extra dev-only mount cycle as a debugging aid, not a bug, when writing custom hooks or subscriptions.

    function useChatRoom(roomId) {
      useEffect(() => {
        const connection = createConnection(roomId);
        connection.connect();
        return () => connection.disconnect(); // must fully undo connect()
      }, [roomId]); // Strict Mode connects, disconnects, reconnects in dev to prove cleanup is correct
    }
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React useEffect Hook
Chapter 23 · Page 167
Intermediate

React useEffect Hook

(FAQ)

FAQ

useEffect runs after the browser has painted the screen, not during render. This makes it safe for DOM mutations and async operations without blocking the UI.

In development, React 18+ Strict Mode intentionally runs setup, cleanup, then setup again for every effect on mount — even with an empty dependency array. This is not a bug: it simulates remounting to surface effects with missing or broken cleanup. Production builds always run the effect once. Fix the underlying issue (incomplete cleanup) rather than trying to suppress the double run.

An infinite loop usually means an object or array is listed as a dependency but gets recreated on every render, so Object.is sees a new reference each time. Move the value outside the component, memoize it with useMemo/useCallback, or restructure so only primitives are in the dependency array.

useLayoutEffect fires synchronously after DOM mutations but before the browser paints, so it blocks rendering — use it only when you need to measure or mutate the DOM before the user sees it. Prefer useEffect for everything else.

Use an AbortController: create it at the top of the effect, pass signal to fetch, and call controller.abort() in the cleanup function. This prevents setState from being called on an unmounted component when the request resolves late.