Search across all documentation pages
Synchronize a component with an external system by running side effects after render.
Quick-reference recipe card — copy-paste ready.
// Run after every render
useEffect(() => {
console.log("rendered");
});
// Run once on mount
useEffect(() => {
fetchData();
}, []);
// Run when dependencies change
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
// Cleanup on unmount or before re-running
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);When to reach for this: You need to synchronize with something outside React — fetching data, subscribing to events, manipulating the DOM, or setting up timers.
"use client";
import { useEffect, useState } from "react";
export function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
function
What this demonstrates:
resize) inside useEffect[] so the effect runs only once on mountuseEffect runs after the browser paints the screen, not during render| Parameter | Type | Description |
|---|---|---|
setup | () => void or () => () => void | Effect function; optionally returns a cleanup function |
dependencies | unknown[] or omitted | Array of reactive values the effect depends on |
| Return | Type | Description |
|---|---|---|
| (none) | void | useEffect does not return a value |
Data fetching with cleanup (abort controller):
useEffect(() => {
const controller = new AbortController();
async function load() {
const res = await fetch(`/api/users/${id}`, {
signal: controller.signal,
});
const data =
Connecting to a WebSocket:
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (e) => setMessages((prev) => [...prev, JSON.parse(e.data)]);
return () =>
Syncing with localStorage:
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);// Effect with async — must wrap because useEffect cannot return a Promise
useEffect(() => {
async function fetchData() {
const res = await fetch("/api/data");
const json: DataType = await res.json();
setData(json);
}
Infinite loop — Updating state that is also a dependency causes the effect to re-run endlessly. Fix: Narrow your dependency array or move the state update behind a condition.
Missing dependencies — Omitting a value from the dependency array causes stale data inside the effect. Fix: Include all reactive values used inside the effect, or refactor to remove the dependency.
Async directly in useEffect — Returning a Promise instead of a cleanup function produces a React warning. Fix: Define an async function inside the effect and call it immediately.
Race conditions on fast re-renders — If id changes quickly, an older fetch may resolve after a newer one and overwrite correct data. Fix: Use an AbortController or a boolean ignore flag in cleanup.
Running on the server — useEffect does not run during SSR. Accessing window or document without guarding causes build errors. Fix: Access browser APIs only inside useEffect or behind a typeof window !== "undefined" check.
| Alternative | Use When | Don't Use When |
|---|---|---|
useLayoutEffect | You need to measure or mutate the DOM before the browser paints | The effect does not touch layout or visual DOM |
| Event handler | Side effect is triggered by a specific user action (click, submit) | Effect must run on mount or on value change |
Server Component fetch | Data can be fetched at request time on the server | Data depends on client-side state or user interaction |
useSyncExternalStore | Subscribing to an external store (Redux, browser API) | One-time setup or data fetching |
Why not fetch in useEffect? For many apps, frameworks like Next.js offer server-side data fetching that avoids loading spinners and waterfalls. Reserve useEffect fetching for truly client-only data needs.
From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).
// Production example: Keyboard navigation for study cards
// File: src/hooks/use-card-keyboard.ts
import { useEffect } from "react";
interface UseCardKeyboardOptions {
isAutoplayOn: boolean;
cardNumber: number;
totalCards: number;
hasMoreCards: boolean;
onCardClick
What this demonstrates in production:
keydown listener on unmount and whenever dependencies change, preventing stale handlers from accumulating.cardNumber) would cause stale closure bugs where old values are captured.onCardClick, onNext, and onPrev callbacks should be wrapped in useCallback by the parent component. Without that, every parent re-render creates new function references, causing this effect to unsubscribe and resubscribe the listener unnecessarily.isAutoplayOn guard at the top of the handler is a production pattern for disabling keyboard input during automated playback without removing the listener entirely.[] and no dependency array at all?useEffect(() => { ... }, []) runs once on mount and cleans up on unmount.useEffect(() => { ... }) runs after every single render.useEffect expects the callback to return either undefined or a cleanup function.async function returns a Promise, which React cannot use as a cleanup function.useEffect(() => {
async function fetchData() {
const res = await fetch("/api/data");
setData(await res.json());
}
fetchData
AbortController to cancel the previous request when dependencies change.ignore flag set to true in the cleanup function.useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then(res => res.json())
.
useEffect when you need to synchronize with an external system on mount or when a dependency changes.Object.is, which checks reference equality.{} or array [] created during render has a new reference every time.useMemo or extracting primitive dependencies.useEffect((): (() => void) => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
// TypeScript infers the cleanup type automatically
// in most cases -- explicit typing is rarely needed.useEffect only runs on the client, so window and document are always available inside it.useEffect during server-side rendering.useEffect or behind a typeof window !== "undefined" guard.useEffect runs after the browser paints the screen (asynchronous).useLayoutEffect runs synchronously after DOM mutations but before the browser paints.useLayoutEffect only when you need to measure or mutate the DOM before the user sees it (e.g., tooltips, animations).