useEffect
useEffect lets a component synchronise with something outside React — a network request, a timer, a subscription or a browser API. Knowing when not to use it matters just as much.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Visual explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 1, 2026
Related tutorials
Learning objectives#
- Explain what an effect is for
- Write correct dependency arrays
- Clean up subscriptions, timers and requests
- Recognise effects you do not need
- Prevent infinite render loops
Explanation#
React runs your component to calculate the UI. Effects run after React has updated the screen, and are meant for side effects that reach outside React.
The dependency array lists every reactive value the effect uses. React re-runs the effect when any of them change. [] means “run after the first render only”. No array means “after every render”.
An effect can return a cleanup function. React calls it before re-running the effect and when the component unmounts — use it to clear timers, unsubscribe or abort requests.
You do not need an effect to compute values from props or state (compute them during render), or to respond to a user event (do it in the event handler).
Infinite loops happen when an effect sets state that is also one of its dependencies, or when a dependency is an object or function recreated on every render.
Visual explanation#
Syntax#
useEffect(() => {
// set up: subscribe, start a timer, fetch…
return () => {
// clean up
};
}, [dependency1, dependency2]);
Code example: Fetching data with cleanup#
import { useEffect, useState } from 'react';
export default function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setUser(null);
setError(null);
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(setUser)
.catch(err => {
if (err.name !== 'AbortError') setError(err.message);
});
return () => controller.abort(); // cancel if userId changes or we unmount
}, [userId]);
if (error) return <p role="alert">Could not load user: {error}</p>;
if (!user) return <p>Loading…</p>;
return <h2>{user.name}</h2>;
}
Shows “Loading…”, then the user’s name. Switching userId quickly cancels the stale request, so an old response can never overwrite a newer one.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- The effect depends on userId, so it runs on mount and whenever userId changes.
- An AbortController is created for this run of the effect.
- fetch does not reject on HTTP errors, so res.ok is checked explicitly.
- The cleanup aborts the request. If userId changes before the response arrives, the old request is cancelled and cannot set stale data.
- Abort errors are ignored because they are expected during cleanup.
Real-world use case#
Loading data when a route parameter changes, subscribing to a WebSocket, syncing document.title, or attaching a window resize listener.
Common mistakes#
This causes an extra render and can drift out of sync. Compute during render instead.
// ✗ unnecessary effect
const [fullName, setFullName] = useState('');
useEffect(() => setFullName(first + ' ' + last), [first, last]);
// ✓ compute during render
const fullName = first + ' ' + last;
An object literal is a new reference on every render, so the effect re-runs, sets state, re-renders, and repeats.
// ✗ options is new every render → effect runs forever
const options = { page };
useEffect(() => { load(options).then(setData); }, [options]);
// ✓ depend on the primitive value
useEffect(() => { load({ page }).then(setData); }, [page]);
Leaving out a value the effect uses gives you stale data. Enable the react-hooks/exhaustive-deps ESLint rule.
If something should happen because the user clicked, do it in the click handler, not in an effect watching a flag.
Best practices#
- Use effects only to synchronise with external systems
- Include every reactive value in the dependency array
- Always clean up subscriptions, timers and in-flight requests
- Prefer a data-fetching library (TanStack Query, React Router loaders) for complex apps
- Expect effects to run twice in development Strict Mode — write cleanup so this is harmless
Remember this#
Effects sync React with the outside world (network, timers, subscriptions).
Dependency array: [] = once after mount; [x] = when x changes.
Started something? Return a cleanup that stops it.
Practice exercise#
Your task: Write a component that shows the window width and updates it on resize. Make sure the listener is removed on unmount.
Need a hint?
Show solution
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return <p>Width: {width}px</p>;
}
Mini quiz#
When does an effect with an empty dependency array [] run?
Want more? Practise all React MCQs · React interview questions
My notes#
Saved in this browser.