When effects run, cleanup timing, and dependency tracking across the component lifecycle.
useEffect is the primary hook for synchronizing React components with external systems β browser APIs, third-party libraries, network requests, and subscriptions. It runs after the browser has painted the screen, making it non-blocking. Every effect optionally returns a cleanup function that React calls before running the effect again and when the component unmounts.
React commits DOM mutations, the browser paints the screen, and then React asynchronously runs your useEffect callbacks. This order means effects never block the initial render β the user sees content immediately.
React compares the current deps array against the previous render's deps using Object.is() equality. If any dep changed, the effect is scheduled to re-run. No deps array = run after every render. Empty array [] = run once after mount.
If your effect returns a function, React stores it as the cleanup. Before running the effect again (because a dep changed), React calls the previous cleanup first. When the component unmounts, React calls the last cleanup.
In StrictMode, React intentionally mounts β unmounts β remounts every component to surface effects that don't clean up properly. The double invocation only happens in development. This is why proper cleanup is non-negotiable.
useLayoutEffect has identical semantics to useEffect but fires synchronously after DOM mutations and before the browser paints. Use it when your effect needs to read DOM measurements and apply corrections before the user sees anything.
Controls re-run frequency. [] = mount only. [a, b] = when a or b changes. No array = every render. React compares with Object.is() β avoid objects/arrays as deps.
The function returned from your effect. Called before the effect re-runs (dep changed) and when the component unmounts. Used to cancel fetches, clear timers, unsubscribe.
When an effect captures state/props from an old render but never re-runs to get the updated values. Fixed by adding the variable to deps or using a ref.
Fires synchronously after DOM mutation, before paint. Blocks painting β keep it fast. Use for DOM measurements (getBoundingClientRect, scroll position).
Fires before any DOM mutation. Intended for CSS-in-JS libraries to inject style rules synchronously before layout. Not for general application code.
1// 1. Event subscription β must clean up2useEffect(() => {3 window.addEventListener('resize', handleResize);4 return () => window.removeEventListener('resize', handleResize);5}, []); // mount once, clean up on unmount67// 2. Fetch with cancellation (AbortController)8useEffect(() => {9 const controller = new AbortController();1011 fetch('/api/data', { signal: controller.signal })12 .then(res => res.json())13 .then(setData)14 .catch(err => {15 if (err.name !== 'AbortError') throw err; // ignore cancellation16 });1718 return () => controller.abort(); // cancel if dep changes or unmount19}, [userId]);2021// 3. useLayoutEffect for DOM measurement22useLayoutEffect(() => {23 const height = ref.current.getBoundingClientRect().height;24 setContainerHeight(height); // applied before paint β no flicker25}, [content]);
useEffect is the most commonly misused hook in React. Getting effects right β correct deps, proper cleanup, right timing β is essential for building apps that don't leak memory, don't make extra network requests, and don't have stale data bugs. The mental model shift is: 'effects are synchronization mechanisms', not 'lifecycle events'. You're telling React how to stay in sync with the outside world, not hooking into mount/update/unmount callbacks.
A developer writes `useEffect(() => { fetchData() })` without a dependency array. The app makes thousands of API calls per second, the browser freezes, and the API rate limits the user.
No dependency array means the effect runs AFTER EVERY RENDER. fetchData sets state β state change triggers re-render β re-render fires the effect β effect calls fetchData β sets state β infinite loop. Each loop iteration also creates a new network request.
Add the correct dependency array: `useEffect(() => { fetchData() }, [])` for mount-only, or `useEffect(() => { fetchData(query) }, [query])` for re-fetch when query changes. The dependency array tells React's effect scheduler when to skip re-execution.
Takeaway: The dependency array is not optional decoration β it's the core control mechanism of useEffect. No array = every render. Empty array = mount only. Specific deps = run when those values change. Getting this wrong is the #1 source of useEffect bugs.
A countdown timer displays the time remaining. You use `setInterval` inside useEffect with an empty dependency array. The timer shows the same number forever instead of counting down.
The useEffect callback closes over the initial `count` value (e.g., 60). setInterval calls `setCount(count - 1)` every second, but `count` is always 60 inside the closure (captured at mount time). Result: `setCount(59)` every second β never changes from 59.
Use the functional updater form: `setCount(prev => prev - 1)`. The updater receives the CURRENT state, not the stale closure value. Alternatively, store count in a ref and sync it every render. The functional form is React's escape hatch for stale closures in effects.
Takeaway: Effects capture values at the time they're created (closures). For intervals and timeouts that need current state, always use the functional updater `setState(prev => ...)` or a ref pattern to bypass the stale closure.