Skip to main content

Command Palette

Search for a command to run...

Mastering useEffect in React: a practical guide

Updated
8 min read
I
Full-Stack Developer @ Bug0 | Node.js, Next.js, JavaScript, TypeScript | Ex-Wipro, DXC, Majesco/Mastek | ex-Java, ex-Spring

useEffect is one of React’s most important hooks for handling side effects in function components: data fetching, subscriptions, timers, DOM mutations after render, and more. This article explains how it works, common patterns, pitfalls, and best practices, with practical examples and TypeScript notes.


What is useEffect?

useEffect lets you perform side effects after a component renders. Unlike render logic, effects run after React has updated the DOM. The hook signature:

useEffect(effect: () => void | (() => void | undefined), deps?: any[])
  • effect is a function that optionally returns a cleanup function.
  • deps is an optional dependency array that controls when the effect runs.

Basic behavior

  • No dependency array: useEffect(() => { ... }) runs after every render.
  • Empty array: useEffect(() => { ... }, []) runs once after the initial mount.
  • With dependencies: useEffect(() => { ... }, [a, b]) runs after mount and whenever a or b changes (by reference equality).
  • Cleanup: If effect returns a function, React calls it before the next effect run and on unmount.

Example — run once on mount:

useEffect(() => {
  console.log('mounted');
}, []);

Example — cleanup on unmount:

useEffect(() => {
  const id = setInterval(() => console.log('tick'), 1000);
  return () => clearInterval(id); // cleanup
}, []);

Common patterns

  1. Data fetching (with cancellation)
useEffect(() => {
  const controller = new AbortController();
  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(setData)
    .catch(err => {
      if (err.name !== 'AbortError') console.error(err);
    });
  return () => controller.abort(); // cancel fetch on unmount or dep change
}, [/* dependencies */]);
  1. Avoid async directly on effect:
useEffect(() => {
  let canceled = false;
  async function load() {
    const data = await fetchData();
    if (!canceled) setData(data);
  }
  load();
  return () => { canceled = true; };
}, [/* deps */]);
  1. Event listeners
useEffect(() => {
  function onResize() { /* ... */ }
  window.addEventListener('resize', onResize);
  return () => window.removeEventListener('resize', onResize);
}, []);
  1. Subscriptions (WebSocket, RxJS)
useEffect(() => {
  const sub = observable.subscribe(value => setValue(value));
  return () => sub.unsubscribe();
}, [observable]);
  1. Timers
useEffect(() => {
  const id = setTimeout(() => /* ... */, 500);
  return () => clearTimeout(id);
}, [/* deps */]);

Dependency array: rules and gotchas

  • Always include all external values referenced inside the effect (props, state, functions) in the dependency array.
  • Values are compared by reference. Objects, arrays, functions that are re-created each render will re-trigger the effect.
  • To avoid unnecessary re-runs:
    • Use useCallback for stable function references.
    • Use useMemo for expensive objects/arrays.
    • Move logic outside component if it doesn’t depend on props/state.
  • ESLint plugin react-hooks (exhaustive-deps) will warn when dependencies are missing. Prefer following it; if you intentionally omit a dependency, document why with a comment.

Example pitfall — inline object triggers:

useEffect(() => {
  // This runs every render because options is recreated each render
  doSomething({ page: 1 });
}, [{ page: 1 }]);

Fix by memoizing options:

const options = useMemo(() => ({ page: 1 }), []);
useEffect(() => doSomething(options), [options]);

Cleanup semantics

  • Cleanup runs:
    • Before the next invocation of the effect (except the first run).
    • When the component unmounts.
  • This makes effects perfect for managing subscriptions, timers, and external resources.

Edge behavior: if an effect schedules side-effects later (e.g., setting state in a callback), ensure cleanup prevents acting on an unmounted component.


Async effects and cancellation

  • You cannot make the effect callback async (it would return a Promise, which React treats as a non-cleanup value). Use an inner async function or handle promises manually.
  • For fetches: prefer AbortController or a mounted flag pattern to ignore stale responses.

Example with AbortController:

useEffect(() => {
  const ac = new AbortController();
  async function load() {
    try {
      const res = await fetch(url, { signal: ac.signal });
      const json = await res.json();
      setData(json);
    } catch (err) {
      if (err.name !== 'AbortError') console.error(err);
    }
  }
  load();
  return () => ac.abort();
}, [url]);

Multiple effects vs single effect

  • Prefer multiple focused effects rather than one giant effect handling many unrelated concerns. Separation improves readability and reduces accidental re-runs.
useEffect(() => {
  // effect A
}, [a]);

useEffect(() => {
  // effect B
}, [b]);

When to use useLayoutEffect

  • useLayoutEffect runs synchronously after DOM mutations but before browser paint. Use it when you must measure DOM (e.g., reading layout and synchronously applying changes).
  • On server-side rendering (SSR), useLayoutEffect emits a React warning. For universal apps, use useEffect unless you need synchronous layout reads or mute the warning conditionally.

Common mistakes and how to avoid them

  • Forgetting dependencies → stale closures and bugs. Use ESLint rule react-hooks/exhaustive-deps.
  • Putting expensive logic in effects that run too often → cause performance issues. Memoize dependencies and move heavy work elsewhere.
  • Not cleaning up subscriptions/timers → memory leaks or duplicate listeners.
  • Setting state in an effect without proper dependency control → infinite loops.
  • Relying on [] to run effects only once when they depend on props. If effect should react to prop changes, include those props in deps.

Checklist when writing useEffect:

  • Which values does the effect read? Add them to deps.
  • Does the effect create a subscription/timer/fetch? Return a cleanup that cancels it.
  • Can any dependency be stabilized using useMemo/useCallback?
  • Can logic be split into smaller effects?

Testing effects

  • With React Testing Library, use waitFor, findBy*, or act to assert changes caused by effects.
  • Mock network requests (fetch/xhr) and timers (jest.useFakeTimers()) when testing timers or time-dependent logic.
  • Example: wait for elements that rely on fetched data using await findByText(...).

TypeScript tips

Effect signature in TS:

useEffect(() => {
  // ...
  return () => { /* cleanup */ };
}, [dep1, dep2]);

No special typing is normally needed. If you use refs with mutable values, annotate them:

const mountedRef = useRef<boolean>(false);
useEffect(() => {
  mountedRef.current = true;
  return () => { mountedRef.current = false };
}, []);

Useful custom hooks (patterns)

  1. useInterval
function useInterval(callback, delay) {
  const savedCallback = useRef();
  useEffect(() => { savedCallback.current = callback; }, [callback]);
  useEffect(() => {
    if (delay == null) return;
    const id = setInterval(() => savedCallback.current(), delay);
    return () => clearInterval(id);
  }, [delay]);
}
  1. useEventListener
function useEventListener(target, type, handler) {
  const saved = useRef(handler);
  useEffect(() => { saved.current = handler; }, [handler]);
  useEffect(() => {
    const t = target && 'addEventListener' in target ? target : window;
    const listener = e => saved.current(e);
    t.addEventListener(type, listener);
    return () => t.removeEventListener(type, listener);
  }, [target, type]);
}
  1. useFetch (simple)
function useFetch(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    const ac = new AbortController();
    fetch(url, { signal: ac.signal })
      .then(r => r.json())
      .then(setData)
      .catch(() => {});
    return () => ac.abort();
  }, [url]);
  return data;
}

Migrating from class lifecycle

  • componentDidMount -> useEffect(..., [])
  • componentDidUpdate -> useEffect(..., [deps])
  • componentWillUnmount -> cleanup function returned by useEffect

Example:

// class
componentDidMount() { this.timer = setInterval(...); }
componentWillUnmount() { clearInterval(this.timer); }

// function
useEffect(() => {
  const t = setInterval(...);
  return () => clearInterval(t);
}, []);

Server-side rendering (SSR) considerations

  • Effects (useEffect) do not run during SSR — they only run on the client — which is good for client-only side effects (e.g., subscriptions, fetches that should only run in browser).
  • If behavior must run on the server as well, perform the work during rendering or in data fetching logic outside useEffect.
  • useLayoutEffect can warn under SSR; detect environment or use useIsomorphicLayoutEffect pattern that maps to useEffect on server.

Best practices summary

  • Keep effects focused and small.
  • Always clean up subscriptions and timers.
  • Follow exhaustive-deps; stabilize dependencies with memoization when appropriate.
  • Avoid async directly on the effect callback; use inner async functions and cancellation.
  • Use multiple effects for different concerns.
  • When reading layout or performing synchronous DOM updates, prefer useLayoutEffect.
  • Test effects with appropriate asynchronous utilities.

Quick reference: common examples

  1. Fetch with cancel
useEffect(() => {
  const ac = new AbortController();
  (async () => {
    try {
      const res = await fetch(url, { signal: ac.signal });
      setData(await res.json());
    } catch (e) {
      if (e.name !== 'AbortError') console.error(e);
    }
  })();
  return () => ac.abort();
}, [url]);
  1. Debounced effect
useEffect(() => {
  const id = setTimeout(() => doSearch(query), 300);
  return () => clearTimeout(id);
}, [query]);
  1. Subscribe to event
useEffect(() => {
  window.addEventListener('mousemove', onMove);
  return () => window.removeEventListener('mousemove', onMove);
}, []);

useEffect gives functional components the full power to interact with external systems and the DOM, but it requires careful dependency management and cleanup. With the patterns above you can write predictable, efficient, and bug-resistant effects.