Skip to content
Documentation GitHub
Logic Errors

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 render
const 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 verify
const initialContent = useMemo(() => {
return page?.blocks[0]?.content || "";
}, [page?.blocks[0]?.content]); // ESLint can't verify this

Fix: Extract to a variable:

const firstBlockContent = page?.blocks[0]?.content;
const initialContent = useMemo(() => {
return firstBlockContent || "";
}, [firstBlockContent]); // simple variable, easily verified

Pattern 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 hook
const 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 referenced
const loadSuggestions = useCallback(async (query: string) => {
// ...
}, []);
const checkTrigger = useCallback((editor) => {
loadSuggestions(query);
}, [loadSuggestions]); // now properly listed

Pattern 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 computation
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, 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 used

Prevention

Best Practices

  • Run oxlint with react-hooks/exhaustive-deps as 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

  • useCallback or useMemo with 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

Was this page helpful?