The useEffect Hook
useState stores data inside a component. useEffect lets a component reach outside itself — to save data, start a timer, subscribe to an event, or fetch from an API. These are side effects: actions that affect something other than what the component renders.
The basic shape
Section titled “The basic shape”import { useEffect } from 'react';
useEffect(() => { // runs after every render by default document.title = `ZeroBudget — ${month}`;});React runs the callback after the component renders and after every subsequent re-render. Putting everything in here without a dependency array re-runs on every render — usually not what you want.
The dependency array
Section titled “The dependency array”The second argument is the dependency array. React re-runs the effect only when the values in that array change:
useEffect(() => { document.title = `ZeroBudget — ${month}`;}, [month]); // re-runs only when month changesEmpty array — runs once, after the first render only:
useEffect(() => { console.log('Component mounted');}, []);Specific values — runs after the first render and whenever those values change:
useEffect(() => { localStorage.setItem('zb-categories', JSON.stringify(categories));}, [categories]);No array — runs after every render. Rarely the right choice.
The golden rule of the dependency array
Section titled “The golden rule of the dependency array”Include every reactive value the effect reads from. If your effect uses categories, categories goes in the array. The React linter (eslint-plugin-react-hooks) enforces this automatically.
Missing a dependency causes stale closure bugs: the effect runs with an outdated copy of the value from when the component last re-rendered with that dependency included.
Cleanup
Section titled “Cleanup”Some effects need to clean up after themselves — clearing a timer, canceling a fetch, removing an event listener. Return a cleanup function from the effect:
useEffect(() => { const id = setInterval(() => setTick(t => t + 1), 1000); return () => clearInterval(id); // cleanup when the component unmounts or before the effect re-runs}, []);React calls the cleanup function before running the effect again (due to a dependency change) and when the component unmounts.
What useEffect is not for
Section titled “What useEffect is not for”useEffect is for synchronizing with external systems. Do not use it to:
- Transform data for rendering — compute that during render instead (derived state)
- Update state based on other state — compute the value directly or use event handlers
- Respond to user events — use event handlers, not effects
If you find yourself writing useEffect(() => { setX(computeFromY(y)); }, [y]), you probably want const x = computeFromY(y) as a regular variable during render.
Exercise
Section titled “Exercise”- Add a
useEffecttoApp.jsxthat logs'Render'to the console on every render. Observe when it fires. - Change it to a
[]dependency array. Confirm it now only fires once. - Add a
useEffectthat saves theincomeSourcesarray tolocalStorageunder the key'zb-income'wheneverincomeSourceschanges. - Reload the page and confirm the value is in localStorage (check the Application tab in DevTools).
useEffectruns side effects after renders — saving to storage, timers, subscriptions, API calls.- The dependency array controls when it re-runs: empty for once-only, specific values to watch for changes.
- Always include every reactive value the effect reads — missing dependencies cause stale closure bugs.
- Return a cleanup function to cancel timers, abort fetches, or remove event listeners.
- Do not use
useEffectto derive values for rendering — compute them directly during render.