6 Secret useEffect Techniques
Beyond fetch and event listeners. What the docs show — and what they don't explain properly.

useEffect is the most taught hook. And the most misunderstood. Not because the API is bad — but because most tutorials stop at the second example.
Fetch on mount. Event listener. Dependency array with the right variable. That's the basics, and the basics are everywhere.
What's not everywhere are the six behaviors that separate those who use useEffect from those who understand useEffect.
Technique 01 — Cleanup with AbortController
Every time a component unmounts before a fetch completes, the response still arrives — and tries to update the state of a component that no longer exists.
React used to warn in the console: "Can't perform a React state update on an unmounted component". The warning was removed in React 18. The bug remains.
function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { const controller = new AbortController(); async function fetchUser() { try { const res = await fetch(`/api/users/${userId}`, { signal: controller.signal, }); const data = await res.json(); setUser(data); } catch (err) { if (err.name === 'AbortError') return; console.error(err); } } fetchUser(); return () => controller.abort(); }, [userId]); return <div>{user?.name}</div>;}Technique 02 — The dependency array is a declaration, not a filter
The wrong instinct shows up like this: the effect runs too often, so the dev removes a dependency from the array to "stop" the extra executions. It works until it doesn't.
The dependency array tells React which values this effect uses. Not which values should trigger the effect. That distinction changes everything. Omitting a dependency doesn't silence the re-run — it creates a stale closure: the effect gets stuck with an old snapshot of the value, and the bug surfaces non-deterministically.
useEffect(() => {
// count is read here,
// but omitted from the array.
// The value never updates.
document.title = `Count: ${count}`;
}, []); // ← lying to React
useEffect(() => {
// count is in the array.
// React knows when to re-run.
document.title = `Count: ${count}`;
}, [count]); // ← honest contract
If the effect runs too often, the real problem is usually an object or function being recreated on every render. The fix is to stabilize the values — with useCallback, useMemo, or by moving the definition inside the effect itself. Not omitting.

Technique 03 — useLayoutEffect: when the DOM can't wait
useEffect fires after the browser paints. Most of the time, that's exactly what you want. But there's a window between React committing changes to the DOM and the browser painting the screen.
For tooltip positioning, layout measurements, and animations that depend on real dimensions — useEffect fires too late. The user sees the element in the wrong position before it corrects itself.
function Tooltip({ targetRef, children }) { const tooltipRef = useRef(null); const [pos, setPos] = useState({ top: 0, left: 0 }); // Measures BEFORE paint — no position flash useLayoutEffect(() => { if (!targetRef.current || !tooltipRef.current) return; const targetRect = targetRef.current.getBoundingClientRect(); const tooltipRect = tooltipRef.current.getBoundingClientRect(); setPos({ top: targetRect.top - tooltipRect.height - 8, left: targetRect.left + targetRect.width / 2 - tooltipRect.width / 2, }); }, [targetRef]); return ( <div ref={tooltipRef} style={{ position: 'fixed', top: pos.top, left: pos.left }}> {children} </div> );}Technique 04 — Refs as dependency escape hatches
Callback functions come as props. When you need to call them inside an effect, the linter requires them in the dependency array. But callbacks recreated on every render make the effect re-run for no real reason.
The ref is the way out: it stores the latest value without making the effect depend on it.
function useInterval(callback, delay) { // Ref always holds the latest version of the callback const savedCallback = useRef(callback); // Updates the ref without re-running the effect useEffect(() => { savedCallback.current = callback; }, [callback]); // The interval uses the ref — deps array only needs [delay] useEffect(() => { if (delay === null) return; const id = setInterval(() => { savedCallback.current(); // reads latest value via ref }, delay); return () => clearInterval(id); }, [delay]);}Technique 05 — useEffectEvent: reading state without depending on it
An effect needs to react to a change — roomId — but inside it, needs to read a current value — theme — without that value causing re-execution. React 19 introduced useEffectEvent to formalize this separation between reactive and non-reactive code.
import { experimental_useEffectEvent as useEffectEvent } from 'react';function ChatRoom({ roomId, theme }) { // onConnected reads theme internally, but doesn't depend on it const onConnected = useEffectEvent(() => { showNotification(`Connected to room ${roomId}`, theme); }); useEffect(() => { const connection = createConnection(roomId); connection.on('connected', () => onConnected()); connection.connect(); return () => connection.disconnect(); }, [roomId]); // theme doesn't need to be here}Technique 06 — useEffect doesn't manage derived state
This is the most common silent mistake. The code works. Tests pass. And yet there's an extra render every time — plus a useEffect that shouldn't exist.
// ❌ Effect to derive state — causes double renderfunction FilteredList({ items, query }) { const [filtered, setFiltered] = useState([]); useEffect(() => { setFiltered(items.filter(item => item.name.includes(query))); }, [items, query]); return <List items={filtered} />;}// ✅ Calculated during render — correct and no overheadfunction FilteredList({ items, query }) { const filtered = useMemo( () => items.filter(item => item.name.includes(query)), [items, query] ); return <List items={filtered} />;}useEffect exists to synchronize React with external systems: APIs, WebSockets, timers, localStorage, third-party libraries. Not to transform data that's already inside React.
useEffect hasn't changed much since React 16.8. What changed is the understanding of what it actually is: a synchronization layer between React and the external world.
Everything that doesn't need the external world — doesn't belong there.
Test your knowledge
Answer and check your understanding.


