useEffect & the component lifecycle
Mount, update and unmount, dependency arrays, cleanup, Strict Mode and when not to use effects.
Most of your component code is pure rendering: props and state in, JSX out. But apps also need to talk to the outside world — set the page title, start timers, subscribe to events or websockets, measure the DOM, sync with a non-React widget, or fetch data. useEffect is React's tool for synchronising a component with an external system. This lesson explains exactly when effects run, how dependencies and cleanup work, what Strict Mode is doing, and when you don't need an effect at all.
The basic shape#
useEffect(setup, dependencies):
- React renders your component and updates the screen.
- Then it runs
setup. Effects never block painting. - On later renders, React compares each dependency with the previous render's value. If any changed, it runs the cleanup (if any) and then
setupagain.
The component lifecycle#
Every component goes through three phases:
- Mount — it appears on screen for the first time.
- Update — it re-renders because its props or state changed.
- Unmount — it's removed from the screen.
Effects let you hook into these moments, but the better mental model is synchronisation: "keep this external thing in sync with these values". React starts the sync on mount, re-syncs when values change, and stops on unmount.
Dependency arrays#
Dependencies aren't a choice
The rule: every reactive value used inside the effect (props, state, and variables or functions declared in the component body) must be in the dependency array. The react-hooks/exhaustive-deps lint rule checks this. If you leave one out, the effect keeps using a stale value from an old render:
The fix here isn't to add count (that would restart the interval every second) but to remove the dependency with an updater function:
If the linter wants a dependency you don't want, change the code so it doesn't need it — don't silence the linter.
Objects and functions as dependencies
Dependencies are compared with Object.is. An object or function created during render is a new value every render, so it re-runs the effect every time:
Cleanup#
If your effect starts something, the cleanup stops it:
React runs the cleanup:
- before re-running the effect (with the old values), and
- when the component unmounts.
Typical pairs: addEventListener/removeEventListener, setInterval/clearInterval, connect/disconnect, subscribe/unsubscribe, start a request/abort() it.
Strict Mode runs effects twice (in development)#
With <StrictMode> (on by default in Vite projects), React mounts every component, unmounts it, and mounts it again in development. You'll see effects run, clean up and run again:
This is deliberate: it simulates the user navigating away and back, and immediately exposes effects that don't clean up properly (duplicate subscriptions, two intervals, leaking listeners). If your effect breaks when run twice, it needs a cleanup. Production builds run effects once.
Don't "fix" it with a ref that prevents the second run — fix the cleanup.
Effects run after paint; useLayoutEffect runs before#
Occasionally you need to measure the DOM and adjust before the user sees anything (e.g. positioning a tooltip). useLayoutEffect has the same API but runs synchronously after DOM updates and before the browser paints. It blocks painting, so use it only for measure-then-adjust layout work; useEffect is right 99% of the time.
You might not need an effect#
Effects are an escape hatch from React. Many beginners overuse them; unnecessary effects make code slower, more complex and buggier. Common cases where you don't need one:
1. Deriving data from props or state
If the calculation is expensive, wrap it in useMemo (see the memoisation lesson) — still no effect.
2. Responding to user events
Code that runs because the user did something belongs in an event handler. Code that runs because the component is displayed belongs in an effect.
3. Resetting state when a prop changes
4. Notifying a parent
Call the parent's callback in the same event handler that changes the state, rather than in an effect that watches it.
A good effect: syncing with a browser API#
(For subscribing to external stores like this, React also offers useSyncExternalStore, which handles some edge cases for you.)
Effects and data fetching#
Fetching data in an effect works and is covered in the next lesson, but it has pitfalls (race conditions, no caching, waterfalls). In real apps, prefer a library like TanStack Query or your framework's data loading (React Router loaders, Next.js Server Components).
Common mistakes#
- Missing dependencies → stale values. Trust the linter.
- Objects/functions created during render as dependencies → effect runs every render.
- Setting state unconditionally in an effect with no dependency array → infinite loop.
- Forgetting cleanup → duplicate listeners, timers and connections (Strict Mode reveals this).
- Using effects to derive state or handle user events.
- Making the effect callback itself
async— it must return nothing or a cleanup function, not a promise. Define an async function inside and call it.
What's next#
Next, apply all of this to the most common effect of all: fetching data, done properly.
Check your understanding
Quick quiz
1.When does an effect with dependencies
[userId]run?2.What is the cleanup function returned from an effect for?
3.You have
firstNameandlastNamein state and wantfullName. What should you do?
Finished reading?
Mark this lesson complete to track your progress.