useEffect runs a piece of code after React has rendered your component and updated the screen. You use it to keep a component in sync with something outside React, like a timer, a network connection or an event listener, and you control when it runs again with a dependency array.
Open the console under the preview: the effect logs once when the component appears, then again after every click. Change [count] to [] and click again: the effect stays silent after the first log.
The syntax
useEffect(() => {
// setup: runs after the render is on the screen
return () => {
// cleanup: runs before the next setup, and on unmount
};
}, [dependencies]);
- The first argument is the setup function. It can return a cleanup function, or nothing.
- The second argument is the dependency array: every value from the component that the setup reads (props, state, and anything computed from them).
- Call
useEffectat the top level of the component, like every hook.
Effects run after render
Rendering is React calling your component function to find out what the screen should show. Effects wait until that work is finished and the DOM is updated, so inside an effect the page already shows the new values.
Every click logs the render first and the effect second, and the effect reads the button text React just wrote. This is why reading the DOM, starting a timer or connecting to a server belongs in an effect and not in the component body: the body runs during rendering, before anything is on the screen, and React may call it more often than you expect.
Effects usually run after the browser has painted. When an effect must measure the DOM before paint, use useLayoutEffect instead, which always runs before paint.
The dependency array: three shapes
The dependency array decides when the effect runs again. There are three shapes, and this block uses all of them so you can watch the difference.
On load all three effects log. Click a + 1 and two of them run: the one with no array and the one that depends on a. Click b + 1 and only the one with no array runs, because nothing in [] or [a] changed.
| Dependency array | When the effect runs |
|---|---|
| none | After every render |
[] | Once, after the first render (on mount) |
[a, b] | After the first render, then whenever a or b changed |
React compares each dependency with its previous value using Object.is. Numbers, strings and booleans compare by value. Objects, arrays and functions compare by identity, which matters a lot (see below).
Do not leave a value out of the array to make an effect run less often. The effect would keep using the value from an old render. The react-hooks/exhaustive-deps lint rule catches this; it is part of the ESLint setup that Vite's React template and Next.js's ESLint config include.
Cleanup
Many effects start something that has to be stopped: a connection, a subscription, a timer. Return a function from the effect and React calls it at two moments: right before the effect runs again with new dependencies, and when the component leaves the screen (unmount).
Pick another room: the console shows disconnect from general and then connect to music. The old connection always closes before the new one opens. Click Hide and the cleanup runs one last time, because ChatRoom was unmounted.
The function connect here is a stand-in for a real chat or WebSocket client. The shape is the same for any of them: create it in the setup, close it in the cleanup.
Clearing a timer
A timer started in an effect keeps running after the component is gone unless the cleanup stops it.
Delete the clearInterval(id); line, then hide the timer: tick keeps arriving in the console from a component that no longer exists. Each Show starts another one.
Note setSeconds((s) => s + 1). The effect runs once, so a plain setSeconds(seconds + 1) would read seconds from the first render forever and stop at 1. The updater function always gets the latest value.
Removing an event listener
Event listeners on window or document follow the same pattern. Pass the same function to removeEventListener that you passed to addEventListener:
useEffect(() => {
function handleKeyDown(e) {
if (e.key === 'Escape') onClose();
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
The infinite loop
An effect that sets a state value it also depends on starts a loop: the state changes, React renders, the dependency changed, the effect runs and sets the state again.
// Never stops: every render runs the effect, every effect causes a render
useEffect(() => {
setCount(count + 1);
});
// Same loop: count changes, so the effect runs again
useEffect(() => {
setCount(count + 1);
}, [count]);
The component renders without end and the tab stays busy. React does not stop this loop: in development it logs a "Maximum update depth exceeded" error to the console, and in production it logs nothing. This block runs the second version with a stop condition, so you can watch the loop without freezing the page:
The console logs render 0 through render 5: six renders for one mount, each one started by the effect. The fixes depend on what you meant:
- You wanted it once. Use
[]and the updater form,setCount((c) => c + 1), socountis not a dependency. - You are computing a value from other state. Do not store it at all; compute it during render, as shown in the last section of this page.
- You are reacting to a user action. Put the code in the event handler.
Objects and functions as dependencies
An object, array or function created in the component body is a new value on every render, so an effect that depends on it runs after every render, even when nothing inside it changed.
Toggling the colors has nothing to do with the search, yet every toggle logs search for react again. Move the options line inside the effect and change the array to [query]: toggles go quiet and only typing in the input runs the search.
That is the general fix. Create objects and functions inside the effect that uses them, and depend on the primitive values they are built from. When a function must live outside the effect, useCallback keeps its identity stable between renders.
Why effects run twice in development
With <StrictMode> on (a new Vite app wraps the root in it, and the Next.js App Router turns it on by default), React runs a test in development: it mounts each component, runs its effects, runs their cleanups, and runs the effects again. For the chat room above, the development console shows:
connect to general
disconnect from general
connect to general
This happens once, on mount, and only in development. It exists to expose effects that are missing a cleanup: if two runs in a row break something, the user would break it too by leaving and coming back. The fix is never to block the second run with a ref; it is to write the cleanup. The preview on this page runs like a production build, so you will not see the double run here.
You might not need an effect
Effects are for syncing with systems outside React. Two common uses are better without one.
Values computed from props or state. Calculate them during render. Storing them in state and syncing with an effect costs an extra render, and the first of those renders puts a stale value in the DOM:
Each click renders WithEffect twice, and its first render still shows the old name. Computed renders once with the right value.
Code that responds to a user action. Sending a form, showing a toast after a purchase, or posting an analytics event for a click belongs in the event handler. You know exactly what happened there, while an effect only knows that some value changed.
useEffectEvent
Sometimes an effect needs the latest value of a prop without re-running when that prop changes. In the chat room, a notification should use the current theme, but switching the theme should not reconnect. React 19.2 adds useEffectEvent for this:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // always the latest theme
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => onConnected());
connection.connect();
return () => connection.disconnect();
}, [roomId]); // theme is not a dependency
}
A function from useEffectEvent is only for calling inside effects. Do not pass it to other components or list it in a dependency array.
Frequently Asked Questions
When does useEffect run?
After React has rendered the component and updated the DOM, usually after the browser has painted. With no dependency array it runs after every render, with [] only after the first one, and with [a, b] after the first render and after any render where a or b changed.
What is the useEffect cleanup function?
The function you return from the effect. React calls it before running the effect again with new dependencies, and once more when the component is removed from the screen. Use it to clear timers, remove event listeners and close subscriptions.
Why does my useEffect run twice?
In development, <StrictMode> mounts every component, runs its effects, cleans them up and runs them again, to show you effects that are missing a cleanup. It happens once on mount and only in development. Production builds run the effect once.
Why does my useEffect cause an infinite loop?
The effect sets a state value that is also in its dependency array (or it has no array at all). Setting the state renders again, the dependency changed, so the effect runs again. Use an updater function and remove the dependency, add a condition, or compute the value during render instead.
Can the useEffect callback be async?
No. The callback must return nothing or a cleanup function, and an async function returns a promise. Define an async function inside the effect and call it, as shown on the fetching data page.