React useEffect Hook
A side effect is anything a component does that reaches outside itself: fetching data from an API, subscribing to an event, updating the browser title, starting a timer, or writing to localStorage. These actions cannot happen directly during rendering, because rendering should be pure and predictable.
useEffect gives you a safe place to run side effects after React has finished rendering the component.
How useEffect works
useEffect takes two arguments: a function containing the side effect, and an optional dependency array.
useEffect(() => {
// Your side effect goes here
}, [/* dependencies */]);
React runs the function after every render by default. The dependency array controls when it runs.
Controlling when the effect runs
Run after every render
Omit the dependency array entirely. React runs the effect after every render and re-render.
useEffect(() => {
console.log('Component rendered');
});
This is rarely what you want. Most effects should only run when something specific changes.
Run once on mount
Pass an empty array. React runs the effect once, after the component first appears on screen. This is the pattern for fetching initial data.
useEffect(() => {
fetchData();
}, []);
The empty array tells React: “this effect does not depend on any values from the component, so it never needs to re-run.”
Run when specific values change
List the values you depend on. React runs the effect after the first render and again any time one of those values changes.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
React compares each dependency to its previous value before deciding whether to re-run the effect. If none have changed, it skips the effect.
Fetching data
The most common use of useEffect is to load data from an API when a component mounts.
import { useState, useEffect } from 'react';
export default function GitHubUsers() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUsers() {
const response = await fetch('https://api.github.com/users');
const data = await response.json();
setUsers(data);
setLoading(false);
}
fetchUsers();
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>
<img src={user.avatar_url} alt={user.login} width={32} />
{user.login}
</li>
))}
</ul>
);
}
Note that the async function is defined inside the effect, not on the effect itself. See the FAQ below for why.
For real projects, consider using TanStack Query
(formerly React Query) or SWR
instead of writing fetch logic inside useEffect directly. These libraries handle caching, loading and error states, background refetching, and request deduplication automatically.
Cleanup functions
Some side effects need to be cleaned up when the component unmounts or before the effect re-runs. You do this by returning a function from the effect.
Cleaning up a timer
useEffect(() => {
const id = setInterval(() => {
console.log('tick');
}, 1000);
return () => clearInterval(id); // Cleanup: cancel the timer
}, []);
Without the cleanup, the timer would keep running even after the component is removed from the page.
Cleaning up an event listener
useEffect(() => {
function handleKeyDown(event) {
if (event.key === 'Escape') {
closeModal();
}
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [closeModal]);
React calls the cleanup function before running the effect again with new values, and when the component unmounts.
Combining effects and cleanup
import { useState, useEffect } from 'react';
function RepeatMessage({ message }) {
useEffect(() => {
const id = setInterval(() => {
console.log(message);
}, 2000);
return () => clearInterval(id);
}, [message]);
return <p>Logging: "{message}"</p>;
}
export default function App() {
const [message, setMessage] = useState('Hello');
return (
<div>
<input
value={message}
onChange={e => setMessage(e.target.value)}
/>
<RepeatMessage message={message} />
</div>
);
}
Each time message changes, React clears the old interval and starts a new one. Without cleanup, old intervals would stack up.
React 18 and StrictMode
In development, if your app is wrapped in <React.StrictMode> (which Vite and Next.js both do by default), React intentionally runs each effect twice on mount: it mounts, runs the effect, cleans up, then runs the effect again.
This does not happen in production. It is a development-only tool to help you find bugs in effects that are missing cleanup functions.
If your effect runs twice and causes a problem, that is a sign your cleanup function is incomplete or missing.
useEffect(() => {
const connection = openConnection();
return () => connection.close(); // Cleanup ensures the double-run is harmless
}, []);
If you see an effect running twice and wonder why, StrictMode in development is the reason.
Common mistakes
Missing dependencies
If your effect uses a variable from the component but you leave it out of the dependency array, the effect will use a stale (outdated) copy of that variable.
// Bug: effect always sees count = 0
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count); // Always logs 0
}, []); // count is missing from the deps array
// Correct
useEffect(() => {
console.log(count);
}, [count]);
The eslint-plugin-react-hooks
package includes an exhaustive-deps rule that catches this automatically.
Causing an infinite loop
If you call a state setter inside an effect without a dependency array (or with a dependency that changes on every render), you create an infinite loop.
// Infinite loop: effect sets state, state causes render, render runs effect
useEffect(() => {
setCount(count + 1);
}); // No dependency array = runs after every render
Putting an object or function in the dependency array
Objects and functions are recreated on every render. If you list a function or object defined in the component as a dependency, the effect will re-run on every render even if the underlying data has not changed.
Use useCallback to stabilise a function before adding it as a dependency, or move the function definition inside the effect.
FAQ
Why does my effect run twice in development?
If your app uses <React.StrictMode>, React deliberately mounts and unmounts your component once on startup in development to help you spot missing cleanup functions. This is intentional and does not happen in production. See the section above for details.
When should I use useEffect vs useLayoutEffect?
useEffect runs asynchronously after the browser has painted. useLayoutEffect runs synchronously before the browser paints, which lets you measure the DOM and make changes before the user sees anything. Use useEffect for almost everything. Use useLayoutEffect only when you need to read or change the DOM layout and cannot afford the brief visual flicker that useEffect would cause (for example, measuring an element’s size to position a tooltip).
Why can’t I make the useEffect callback async?
useEffect expects its callback to return either nothing or a cleanup function. An async function always returns a Promise, and React does not know what to do with a Promise as a return value. Instead, define an async function inside the callback and call it immediately:
useEffect(() => {
async function loadData() {
const result = await fetchSomething();
setData(result);
}
loadData();
}, []);
What to learn next
- useState : managing the values your effects depend on
- useRef : persisting values between renders without triggering effects
- TanStack Query docs : a better way to handle data fetching in most cases