React Hooks exhaustive-deps Fix Patterns
React Hooks exhaustive-deps Fix Patterns
Problem
The react-hooks/exhaustive-deps ESLint rule reports warnings when hook dependency arrays don’t match the values used
inside the hook. These aren’t style issues — they catch real bugs where callbacks or effects see stale data after
re-renders.
Symptoms:
- “The ‘X’ array makes the dependencies of useCallback Hook change on every render”
- “React Hook useEffect has a missing dependency: ‘Y’”
- “has a complex expression in the dependency array”
- “has an unnecessary dependency”
Pattern Catalog
Pattern 1: Array/Object Created Every Render
Warning: “The ‘allResults’ array makes the dependencies of useCallback Hook change on every render”
Problem: A new array is created on every render, causing downstream hooks to re-execute:
// BAD — new array reference every renderconst allResults = [...pageResults, ...contentResults, ...actionResults];
const handleKeyDown = useCallback((e: KeyboardEvent) => { const item = allResults[selectedIndex]; // uses allResults}, [allResults, selectedIndex]); // allResults changes every render!Fix: Wrap in useMemo to stabilize the reference:
const allResults = useMemo( () => [...pageResults, ...contentResults, ...actionResults], [pageResults, contentResults, actionResults]);Pattern 2: State Read + Set in Same Effect (Infinite Loop Risk)
Warning: “React Hook useEffect has a missing dependency: ‘expandedSlugs’”
Problem: An effect reads state and also calls the setter for that state. Adding the state to deps causes an infinite loop:
// BAD — reads expandedSlugs but doesn't list it (or infinite loop if listed)useEffect(() => { const newExpanded = new Set(expandedSlugs); // reads state newExpanded.add(someValue); setExpandedSlugs(newExpanded); // sets same state}, [selectedSlug, tree]); // missing expandedSlugs!Fix: Use the functional updater form of the setter, which receives the previous state as an argument and doesn’t need the state in deps:
useEffect(() => { setExpandedSlugs((prev) => { const newExpanded = new Set(prev); // reads via argument, not closure newExpanded.add(someValue); return newExpanded; });}, [selectedSlug, tree]); // no expandedSlugs needed!Pattern 3: Complex Expression in Dependency Array
Warning: “has a complex expression in the dependency array”
Problem: React can’t statically analyze chained optional access in deps:
// BAD — complex expression React can't verifyconst initialContent = useMemo(() => { return page?.blocks[0]?.content || "";}, [page?.blocks[0]?.content]); // ESLint can't verify thisFix: Extract to a variable:
const firstBlockContent = page?.blocks[0]?.content;const initialContent = useMemo(() => { return firstBlockContent || "";}, [firstBlockContent]); // simple variable, easily verifiedPattern 4: Callback References Function Defined Later
Warning: “React Hook useCallback has a missing dependency: ‘loadSuggestions’”
Problem: A callback calls a function that’s defined below it in the file. The empty deps array misses the dependency:
// BAD — loadSuggestions not in deps, and defined after this hookconst checkTrigger = useCallback((editor) => { loadSuggestions(query); // references loadSuggestions}, []); // missing loadSuggestions!
const loadSuggestions = useCallback(async (query: string) => { // ...}, []);Fix: Reorder declarations so the dependency is defined first:
// GOOD — loadSuggestions defined first, then referencedconst loadSuggestions = useCallback(async (query: string) => { // ...}, []);
const checkTrigger = useCallback((editor) => { loadSuggestions(query);}, [loadSuggestions]); // now properly listedPattern 5: Unnecessary Dependency
Warning: “has an unnecessary dependency: ‘suggestions’”
Problem: A value is listed in deps but not actually used inside the hook:
// BAD — suggestions is not used in the memo computationconst position = useMemo(() => { if (!showSuggestions || !editor) return null; const coords = editor.view.coordsAtPos(editor.view.state.selection.from); return { top: coords.bottom, left: coords.left };}, [showSuggestions, editor, suggestions]); // suggestions not used!Fix: Remove the unnecessary dependency:
const position = useMemo(() => { if (!showSuggestions || !editor) return null; const coords = editor.view.coordsAtPos(editor.view.state.selection.from); return { top: coords.bottom, left: coords.left };}, [showSuggestions, editor]); // only what's actually usedPrevention
Best Practices
- Run oxlint with
react-hooks/exhaustive-depsas an error, not a warning - When creating arrays or objects used as deps, always consider
useMemo - Prefer functional updaters (
setState(prev => ...)) whenever an effect reads and sets the same state - Keep dependency expressions simple — extract complex property chains to named variables
- Define helper callbacks before the hooks that reference them
Warning Signs
useCallbackoruseMemowith empty[]deps but referencing outer scope values- Effect that both reads and writes the same state variable
- Spread operators (
[...a, ...b]) directly in component body used as hook dependencies
Mental Model
React deps work by referential equality (Object.is). A new array [...a, ...b] is a new reference every render,
even if contents are identical. Primitive values (strings, numbers, booleans) are compared by value. Understanding this
distinction prevents most exhaustive-deps issues.
References
- React Docs: Rules of Hooks
- React Docs: useCallback
- Commit:
097d2f0— Fixed all 6 exhaustive-deps warnings
Tauri IPC Named-Parameter Wrapping in HTTP Bridge Handlers Next
Rust Async Lock Anti-Patterns: Mutex Across Await and Token Refresh TOCTOU
Was this page helpful?
Thanks for your feedback!