GitHub

useEffect & Render Cycle

When effects run, why the dependency array exists, and how to clean up after yourself.

01

Principle

useEffect is not a lifecycle method — it is a synchronization tool. It answers one question: 'what side effects need to stay in sync with this data?' Every time the dependency array changes, React re-runs the effect to keep things synchronized. When you understand this mental model, dependency arrays stop feeling like magic rules and start making sense.

lightbulb

If you find yourself writing useEffect to fetch data, stop. That is what React Query is for. useEffect is for synchronizing with things outside React — browser APIs, subscriptions, timers.

02

Rules

  • check_circle
    Always declare dependencies honestlyEvery value from the component scope used inside the effect belongs in the dependency array. If you add eslint-disable to hide a missing dependency, you have a bug.
  • check_circle
    Return a cleanup function when neededIf your effect creates a subscription, timer, or event listener — clean it up in the return function. Otherwise you get memory leaks and stale handlers.
  • check_circle
    Empty array means once on mount[] runs the effect once after the first render. Only use this when the effect truly has no dependencies — not as a shortcut to avoid thinking about deps.
  • check_circle
    Do not use useEffect for data fetchingFetching data inside useEffect causes race conditions, no loading state management, and no caching. Use React Query instead.
03

Pattern

hooks/useWindowSize.ts — effect with cleanup
import { useEffect, useState } from 'react';

interface WindowSize {
  width: number;
  height: number;
}

export function useWindowSize(): WindowSize {
  const [size, setSize] = useState<WindowSize>({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    // The effect: subscribe to resize events
    function handleResize() {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }

    window.addEventListener('resize', handleResize);

    // The cleanup: unsubscribe when component unmounts
    // or before the effect re-runs
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // No deps — window never changes

  return size;
}
04

Implementation

info

Version Compatibility

Requires React 19+ and the latest stable versions of all dependencies shown.

In Next.js, window is not available on the server. Guard all browser API access with a typeof window check or use 'use client'.

shared/hooks/useWindowSize.ts
'use client'; // Required — window only exists in browser

import { useEffect, useState } from 'react';

export function useWindowSize() {
  // ✅ Safe initial state — no window access during SSR
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    // ✅ Now safe — this only runs in the browser
    function handleResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    }

    handleResize(); // Set initial size
    window.addEventListener('resize', handleResize);

    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}
menu_book
React Patterns

Helping developers build robust React applications since 2025.

© 2025 React Patterns Cookbook. Built with ❤️ for the community.
react-principles