useEffect & Render Cycle
When effects run, why the dependency array exists, and how to clean up after yourself.
Principle
useEffect is not a lifecycle method — it is a synchronization tool. It answers one question: 'what side effects need to stay in sync with this data?' Every time the dependency array changes, React re-runs the effect to keep things synchronized. When you understand this mental model, dependency arrays stop feeling like magic rules and start making sense.
If you find yourself writing useEffect to fetch data, stop. That is what React Query is for. useEffect is for synchronizing with things outside React — browser APIs, subscriptions, timers.
Rules
- check_circleAlways declare dependencies honestlyEvery value from the component scope used inside the effect belongs in the dependency array. If you add eslint-disable to hide a missing dependency, you have a bug.
- check_circleReturn a cleanup function when neededIf your effect creates a subscription, timer, or event listener — clean it up in the return function. Otherwise you get memory leaks and stale handlers.
- check_circleEmpty array means once on mount[] runs the effect once after the first render. Only use this when the effect truly has no dependencies — not as a shortcut to avoid thinking about deps.
- check_circleDo not use useEffect for data fetchingFetching data inside useEffect causes race conditions, no loading state management, and no caching. Use React Query instead.
Pattern
import { useEffect, useState } from 'react'; interface WindowSize { width: number; height: number; } export function useWindowSize(): WindowSize { const [size, setSize] = useState<WindowSize>({ width: window.innerWidth, height: window.innerHeight, }); useEffect(() => { // The effect: subscribe to resize events function handleResize() { setSize({ width: window.innerWidth, height: window.innerHeight, }); } window.addEventListener('resize', handleResize); // The cleanup: unsubscribe when component unmounts // or before the effect re-runs return () => { window.removeEventListener('resize', handleResize); }; }, []); // No deps — window never changes return size; }
Implementation
Version Compatibility
Requires React 19+ and the latest stable versions of all dependencies shown.
In Next.js, window is not available on the server. Guard all browser API access with a typeof window check or use 'use client'.
'use client'; // Required — window only exists in browser import { useEffect, useState } from 'react'; export function useWindowSize() { // ✅ Safe initial state — no window access during SSR const [size, setSize] = useState({ width: 0, height: 0 }); useEffect(() => { // ✅ Now safe — this only runs in the browser function handleResize() { setSize({ width: window.innerWidth, height: window.innerHeight }); } handleResize(); // Set initial size window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return size; }