React Effects Guidelines
Primary reference: https://react.dev/learn/you-might-not-need-an-effect
Quick Decision Tree
Before adding useEffect , ask:
-
Can I calculate this during render? → Derive it, don't store + sync
-
Is this resetting state when a prop changes? → Use key prop instead
-
Is this triggered by a user event? → Put it in the event handler
-
Am I syncing with an external system? → Effect is appropriate
Legitimate Effect Uses
-
DOM manipulation (focus, scroll, measure)
-
External widget lifecycle (terminal, charts, non-React libraries)
-
Browser API subscriptions (ResizeObserver, IntersectionObserver)
-
Data fetching on mount/prop change
-
Global event listeners
Common Anti-Patterns
// ❌ Derived state stored separately const [fullName, setFullName] = useState(''); useEffect(() => setFullName(first + ' ' + last), [first, last]);
// ✅ Calculate during render const fullName = first + ' ' + last;
// ❌ Event logic in effect useEffect(() => { if (isOpen) doSomething(); }, [isOpen]);
// ✅ In the handler const handleOpen = () => { setIsOpen(true); doSomething(); };
// ❌ Reset state on prop change useEffect(() => { setComment(''); }, [userId]);
// ✅ Use key to reset <Profile userId={userId} key={userId} />
External Store Subscriptions
For subscribing to external data stores (not DOM APIs), prefer useSyncExternalStore :
// ❌ Manual subscription in effect const [isOnline, setIsOnline] = useState(true); useEffect(() => { const update = () => setIsOnline(navigator.onLine); window.addEventListener('online', update); window.addEventListener('offline', update); return () => { /* cleanup */ }; }, []);
// ✅ Built-in hook for external stores const isOnline = useSyncExternalStore( subscribe, () => navigator.onLine, // client () => true // server );
Data Fetching Cleanup
Always handle race conditions with an ignore flag:
useEffect(() => { let ignore = false; fetchData(query).then(result => { if (!ignore) setData(result); }); return () => { ignore = true; }; }, [query]);
App Initialization
For once-per-app-load logic (not once-per-mount), use a module-level guard:
let didInit = false;
function App() { useEffect(() => { if (!didInit) { didInit = true; loadDataFromLocalStorage(); checkAuthToken(); } }, []); }
Or run during module initialization (before render):
if (typeof window !== 'undefined') { checkAuthToken(); loadDataFromLocalStorage(); }