Suggest an editImprove this articleRefine the answer for “How useEffect works in React?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**`useEffect`** is a React hook that runs side effects after a component renders. ```jsx useEffect(() => { document.title = `Count: ${count}`; // Runs after render return () => { /* cleanup on unmount or before next effect */ }; }, [count]); // Re-runs only when count changes ``` **Key:** empty `[]` = runs once on mount. No deps = runs on every render. `[value]` = runs when value changes.Shown above the full answer for quick recall.Answer (EN)Image**`useEffect`** is a React hook that runs side effects after a component renders, without blocking the browser from painting the screen. ## Theory ### TL;DR - `useEffect` runs *after* render, not during. The UI appears first, then the effect fires. - Empty `[]` = runs once on mount. No deps = runs on every render. `[value]` = runs when that value changes. - Always return a cleanup function if your effect sets up a subscription, timer, or event listener. - Need post-render work? `useEffect`. Pure state logic? `useState` or a reducer. - Think of it like a hotel "do not disturb" sign: flip it after checking in, and housekeeping (the side effect) only comes when you change the sign (deps). ### Quick example ```jsx import { useState, useEffect } from 'react'; function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = `Count: ${count}`; // Runs after render }, [count]); // Re-runs only when count changes return <button onClick={() => setCount(count + 1)}>Count: {count}</button>; } // Title updates after each click — not on every possible re-render. ``` After the component renders, React updates the document title. If `count` did not change, the effect skips. Clean and predictable. ### Key difference from class lifecycle methods In class components, you had `componentDidMount`, `componentDidUpdate`, and `componentWillUnmount` as three separate methods. `useEffect` collapses all three into one hook. The return value handles cleanup (unmount), the deps array handles conditional re-runs (did update), and an empty array handles the mount-only case. One mental model instead of three. ### When to use - Fetch data on mount: use `[]` as deps. - Re-fetch when a prop changes: put that prop in deps `[userId]`. - Set up a subscription or timer: return a cleanup function. - Sync something external (document title, localStorage, analytics): include the synced value in deps. - Skip `useEffect` for pure calculations, derived state, or synchronous DOM reads before paint. For the last case, use `useLayoutEffect`. ### How React schedules useEffect React runs effects after the browser has painted the screen. This happens during the commit phase of fiber reconciliation: React finishes DOM mutations, the browser paints, then your effect fires asynchronously via the React scheduler. That is different from `useLayoutEffect`, which runs synchronously after DOM mutations but before the paint. Cleanup runs before the next effect when deps change, and again on unmount. React tracks effects per fiber node using a linked list of hooks, which is why hooks must be called in the same order on every render. ### Common mistakes **Mistake 1: no deps array** ```jsx useEffect(() => { fetchData(); // Runs on every render }); ``` Without a deps array, the effect fires after every single render. If `fetchData` triggers a state update, you get an infinite loop. Add `[]` or the specific values your effect actually reads. **Mistake 2: forgetting cleanup** ```jsx useEffect(() => { const timer = setInterval(tick, 1000); // Missing: return () => clearInterval(timer); }, []); ``` The timer keeps running after the component unmounts. This is one of the most common memory leaks in React apps. Always clean up intervals, subscriptions, and event listeners. **Mistake 3: object in deps** ```jsx const config = { id: 1 }; useEffect(() => { api(config); }, [config]); // New object reference on every render ``` Objects are compared by reference. `{ id: 1 } !== { id: 1 }`, so the effect runs on every render no matter what. Depend on the primitive instead: ```jsx useEffect(() => { api({ id: config.id }); }, [config.id]); // Stable primitive ``` **Mistake 4: the infinite loop trap** ```jsx function BadCounter() { const [count, setCount] = useState(0); useEffect(() => { setCount(count + 1); // count in deps triggers re-render, which re-runs the effect }, [count]); } ``` Setting state that is in your deps causes infinite re-renders. If you need a self-incrementing counter, use a functional update with no deps: ```jsx useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); // No stale closure, no deps needed }, 1000); return () => clearInterval(id); }, []); ``` ### Real-world usage - React Query / TanStack Query: uses `useEffect` internally to trigger initial fetches before the query cache takes over. - Next.js: `useEffect` + `useRouter` for post-hydration redirects (router code cannot run on the server). - Framer Motion: DOM measurements happen inside `useEffect` before animations start. - Redux Toolkit: syncing store state to `window` focus events for tab visibility detection. - AbortController for cancellable fetches: ```jsx useEffect(() => { const abort = new AbortController(); fetch(`/api/user/${userId}`, { signal: abort.signal }) .then(res => res.json()) .then(setUser); return () => abort.abort(); }, [userId]); ``` ### Follow-up questions **Q:** When exactly does `useEffect` run relative to the render? **A:** After the browser paints the screen. React commits DOM changes, the browser paints, then effects fire asynchronously. The user sees the updated UI before any effect runs. **Q:** What is the difference between empty `[]` and no deps? **A:** Empty `[]` runs the effect once after the first render. No deps runs it after every render. These are very different behaviors, and mixing them up causes bugs. **Q:** How do you cancel a fetch when the component unmounts? **A:** Use `AbortController`. Create it inside the effect, pass its signal to fetch, and call `abort.abort()` in the cleanup function. This prevents state updates on unmounted components. **Q:** Why does the ESLint exhaustive-deps rule exist? **A:** To catch stale closures. If your effect reads a variable but does not list it in deps, it captures the value from the first render and never updates. The linter forces you to be explicit about dependencies. **Q:** In React 18 concurrent mode, how do effects behave during transitions? **A:** Effects still run after commit, even when renders are interrupted by transitions. React may render a component multiple times before committing, but effects fire only once per commit. This is why React 18 Strict Mode invokes effects twice in development: to surface bugs in effects that are not idempotent. ## Examples ### Basic: sync document title to state ```jsx import { useState, useEffect } from 'react'; function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = `Count: ${count}`; }, [count]); return <button onClick={() => setCount(count + 1)}>Count: {count}</button>; } ``` The effect fires once after mount (setting the title to "Count: 0"), then again each time the button is clicked. If the component re-renders for another reason without `count` changing, the effect skips. ### Intermediate: fetch user data with cleanup ```jsx import { useState, useEffect } from 'react'; function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { const abort = new AbortController(); fetch(`/api/user/${userId}`, { signal: abort.signal }) .then(res => res.json()) .then(setUser) .catch(err => { if (err.name !== 'AbortError') console.error(err); }); return () => abort.abort(); // Cancel if userId changes before fetch completes }, [userId]); return <div>{user ? user.name : 'Loading...'}</div>; } ``` When `userId` changes, React runs cleanup first (aborting the in-flight request), then starts a new fetch. No race conditions, no state updates from stale responses. ### Advanced: avoiding stale closures with useRef I have seen this trip up experienced developers more than once. An effect captures the callback value at the time it runs, not at the time it was defined. ```jsx import { useState, useEffect, useRef } from 'react'; function SearchInput({ onSearch }) { const [query, setQuery] = useState(''); const onSearchRef = useRef(onSearch); // Keep the ref current on every render, no deps needed useEffect(() => { onSearchRef.current = onSearch; }); useEffect(() => { if (!query) return; const timer = setTimeout(() => { onSearchRef.current(query); // Always uses the latest callback }, 300); return () => clearTimeout(timer); }, [query]); // Re-runs only when query changes return <input value={query} onChange={e => setQuery(e.target.value)} />; } ``` The first effect (no deps) keeps `onSearchRef.current` up to date on every render. The second effect reads from the ref, so it never becomes stale, but only re-runs when `query` changes.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.