DEV Community

Cover image for I Found a useEffect With Three Missing Dependencies Right Next to One With Five Unnecessary Ones. Same Codebase.
Avery
Avery

Posted on

I Found a useEffect With Three Missing Dependencies Right Next to One With Five Unnecessary Ones. Same Codebase.

I was debugging a stale data issue and traced it back to a useEffect that was missing three dependencies it clearly needed. The effect referenced a filter value, a page number, and a sort order, and the dependency array only listed the filter value. Classic stale closure problem, the kind that produces bugs which are maddening to track down because the code looks correct at a glance and passes every manual test you run against it in the moment.

While fixing that one, I scrolled up to a different useEffect in the same file. This one had the opposite problem. Five dependencies listed, but at least two of them were objects that got recreated on every render, meaning the effect fired constantly, far more often than it needed to, doing work that should have run once or twice per user interaction but was instead running dozens of times per second during certain interactions, quietly burning performance in a way that would never show up in a quick glance at the code.

Two useEffect hooks. One file. One clearly under-specified, missing dependencies it needed to function correctly. One clearly over-specified, including dependencies that caused it to fire far more than intended. Neither hook was reasoned through the same way, even though they were written weeks apart in the same codebase, presumably by the same AI working from the same underlying model, on the same general category of problem.

This is the kind of inconsistency that is easy to miss because each individual effect, viewed in isolation, does not look obviously wrong. The under-specified one runs and appears to work most of the time, failing only in the specific interaction sequence that exposes the stale closure. The over-specified one also runs and appears to work, just less efficiently than it should, which rarely triggers the kind of visible failure that gets flagged in a quick review.

Why dependency arrays are uniquely prone to this

Most React patterns that show inconsistency across an AI-generated codebase involve a choice between multiple legitimate approaches, the same way prop drilling has several correct solutions depending on context and none of them is objectively wrong. Dependency arrays are different in an important way. There is technically one correct answer for any given effect: include every reactive value the effect body reads, and only those values.

This makes the inconsistency more surprising than in cases with genuine ambiguity. If there is one correct answer, why does the AI arrive at different, both incorrect, answers in different sessions, sometimes in the same file, sometimes within the same component even?

The reason comes down to what determining the correct dependency array actually requires. It is not a simple lookup or a pattern match against something memorized. It requires tracing every variable referenced inside the effect body, determining whether each one is reactive, meaning it can change between renders, or stable, meaning it never changes for the lifetime of the component. It requires checking whether any of those variables are objects or functions that get recreated on every render even when their contents are conceptually the same. And it requires deciding whether that recreation actually matters for this specific effect, or whether the value is stable enough in practice even though it fails a strict reference equality check.

This is a genuinely effortful, multi-step analysis, more so than most people appreciate when they see a two-line effect with a two-item dependency array sitting quietly in a component. Getting it right consistently requires that full trace happening every single time, not just for effects that look complicated on the surface but for the deceptively simple ones too, since those are exactly the ones where a missed dependency slips through because the effect looked too short and too obvious to warrant careful checking.

Two different failure modes with two different underlying causes

The under-specified failure, missing dependencies that should be there, tends to happen when the effect body is doing something that does not immediately read as depending on a value, even though it functionally does. A function called inside the effect that internally closes over a piece of state, where the dependency on that state is hidden one layer down inside the function definition rather than visible directly in the effect body. A value pulled from a ref that changes over time, where the changing nature of the ref's contents is easy to overlook because refs are correctly excluded from dependency arrays in the general case, leading to an incorrect assumption that anything touching a ref can be safely omitted. A calculated value derived from something that changes, where the derivation happens through an intermediate variable or a small utility function, and the connection back to the original changing source is not obviously visible when scanning the effect body quickly.

The over-specified failure, including dependencies that should not be there as-is or that need to be stabilized first, tends to happen with objects and functions specifically, rather than with primitives like strings or numbers. An options object passed into a hook, recreated fresh on every single render because it was defined inline in the component body using object literal syntax rather than being memoized with useMemo. A callback function that gets included in the dependency array because it is technically referenced inside the effect and technically does change identity on every render, without the generation process recognizing that the actual fix is wrapping that callback in useCallback rather than simply including it and accepting the extra re-runs.

Both failure modes come from the same underlying gap in the reasoning process: correctly determining reactivity requires understanding not just what values are referenced textually inside the effect, but how those values are created upstream and whether their identity is actually stable across renders in practice. That second part, the identity stability question, is where both the under-specification and the over-specification tend to originate, just pulling in opposite directions depending on which piece of that reasoning gets shortcut.

Why this varies so dramatically from session to session

The dependency array analysis is sensitive to exactly how much of the surrounding code the model actually traces through during generation, and that amount of tracing is not consistent across different situations even within the same codebase. If a function used inside the effect is defined a few lines up in the same component, that connection is easy to make because it is locally visible. If the function is imported from a custom hook defined in a completely different file, or if the value comes from several layers of prop passing and transformation before it finally reaches this component, the full trace required to determine reactivity correctly becomes substantially harder to complete reliably during a single generation pass.

This explains why the same underlying codebase can show wildly different quality in its dependency arrays depending on which specific effect you happen to look at. A simple effect in a simple component, where everything referenced is defined two lines above the effect itself and nothing is imported from elsewhere, is much more likely to get a correct dependency array than an effect buried in a component with several custom hooks, prop drilling, and context consumption feeding into it. The complexity of the surrounding code directly affects how reliably the dependency analysis actually gets completed, even though the underlying React rule for what counts as a correct dependency array does not change at all based on that surrounding complexity.

This also explains why fixing one bad dependency array and moving on rarely solves the broader pattern. The specific effect gets corrected, but the underlying situation that made the analysis unreliable, complex surrounding code, indirect references, values passed through several layers, remains exactly the same for the next effect that gets generated in similarly complex surroundings.

What a rule actually needs to specify here

Unlike prop drilling, where the rule needs to define which of several legitimate patterns applies under which conditions, the dependency array problem needs a rule that forces the actual analytical steps to happen rather than being silently skipped when they seem unnecessary for a short, simple-looking effect. The correct answer here is not ambiguous the way prop drilling's correct answer depends on context. The problem is that determining it correctly is effortful enough that the effort sometimes gets shortcut, especially for effects that appear simple at first glance.

Dependency array verification rule:
1. Before finalizing any useEffect, explicitly list every variable, function, and value referenced inside the effect body, including anything referenced indirectly through a called function or a value derived from another value.
2. For each referenced value identified in step one, determine whether it is reactive, meaning it can change between renders, or stable, meaning it never changes for the lifetime of the component.
3. Every reactive value identified in step one belongs in the dependency array without exception, regardless of how the effect appears to behave during casual testing.
4. Any object or function included as a dependency must be verified as stable across renders before being included as-is. If it is not stable, it gets wrapped in useMemo or useCallback before being used as a dependency, rather than being included unstable or omitted entirely to avoid extra effect firing.
5. Do not use the exhaustive-deps lint suppression comment under any circumstances. If the technically correct dependency array causes a visible problem, that problem is a signal that something else in the effect needs restructuring, not that the array itself should be shortened to make the symptom disappear.
6. Any effect with more than three dependencies is a signal to reconsider whether the effect is doing too much and should be split into separate effects with narrower, more independent responsibilities.
Enter fullscreen mode Exit fullscreen mode

This rule does not teach the AI new information about how dependency arrays fundamentally work, since that knowledge was already present and demonstrably correct when asked directly. It forces the explicit trace described in steps one and two to happen as a distinct step in the process, rather than allowing the shortcut of an intuitive, pattern-matched guess about what probably belongs in the array based on how similar effects have looked before.

Why forcing the explicit trace matters more than restating the rule

Simply telling the AI to include all dependencies correctly is restating information it already reliably has, in the same way that telling it not to use array index as a key restates information it already reliably has when asked directly. The actual failure point in both cases is not missing knowledge about what the rule states. It is skipping the effortful trace required to correctly apply that rule to this specific, particular case, under the pressure of generating a larger component quickly.

A rule that forces the trace to happen as an explicit, named step, rather than trusting that the trace happened silently and correctly somewhere in the generation process, closes the gap in a way that simply restating the rule does not. This is a pattern worth recognizing well beyond dependency arrays specifically. Anywhere a correct answer requires multi-step reasoning that can plausibly be shortcut under generation pressure, the effective rule is usually one that forces the intermediate steps to happen explicitly and visibly, not one that simply states what the final correct answer should look like once arrived at.

What this means for a team working across many sessions

For a solo developer, inconsistent dependency arrays produce bugs that get discovered and fixed over time, unevenly, as each one happens to surface. For a team, the problem compounds differently, because multiple developers are generating effects independently, each one working from the same underlying tendency to shortcut the trace under pressure, and nobody has visibility into how the last three effects someone else wrote actually handled the same category of decision.

A team without this rule accumulates a distribution of dependency array quality that mirrors individual session inconsistency, just multiplied across however many developers are contributing. Some effects will be correct. Some will be under-specified. Some will be over-specified. And because none of these failure modes throw an obvious error, the distribution tends to remain invisible until a specific bug forces someone to look closely at one particular effect, the same way my own stale data bug forced me to look closely enough to notice the second, opposite problem sitting right next to it.

What changed after adding the verification rule

Since adding this more detailed key verification rule rather than just a blanket reminder about correct dependencies, both failure modes became noticeably less frequent, and interestingly, they became less frequent in somewhat different ways and to different degrees. The under-specification cases dropped the most substantially, since forcing an explicit enumeration of every referenced value catches the ones that were previously getting missed precisely because they were not obviously connected to the effect at a casual glance. The over-specification cases also dropped, though somewhat less dramatically, since the stability check in step four specifically targets the pattern of including an unstable object or function without addressing why it is unstable in the first place, which requires recognizing the underlying cause rather than just naming the symptom.

The effects that remained genuinely complicated after the rule was in place were, importantly, complicated for legitimate structural reasons rather than because of an incomplete or shortcut dependency trace. A handful of effects did end up getting split into multiple smaller effects as a direct consequence of rule six, which turned out to be a reasonable and useful side effect of forcing more deliberate attention onto what each individual effect was actually responsible for doing.

The prompt does not matter. The rules do.

The dependency array problem looks like it should be simple to fix with a quick reminder, since there genuinely is one technically correct answer for any given effect, unlike patterns with multiple legitimate solutions. But the correct answer requires an effortful, multi-step trace through the surrounding code, and that trace is exactly the part that gets shortcut when generation is moving quickly or when the referenced values are not immediately, visibly obvious from the code sitting directly above the effect.

A rule that simply states the correct answer does not fix this, because the AI already knows the correct answer when asked directly and still produces the inconsistent result during actual generation. A rule that forces the trace to happen as an explicit, named step does fix it, because it removes the option of shortcutting the analysis in favor of a quick pattern match. Look for other places in your codebase where the AI has one clearly correct answer readily available but still arrives at it inconsistently across different sessions, and consider whether the fix needs to force a specific reasoning step to happen rather than simply restate the destination it should already know how to reach.


Want to find where your React project has correct-answer problems being silently shortcut during generation?

I built a free 24 point checklist that helps you identify exactly that. The structural decisions where the AI demonstrably has the right knowledge but skips the reasoning required to apply it consistently across every session.

Get the React AI Clean Code Checklist — free

Avery Code React AI Engineering System

Top comments (0)