Exhaustive Dependencies in React Effects
The `react-hooks/exhaustive-deps` lint rule checks whether an Effect declares the reactive values it reads in its dependency array. Correct dependencies keep the Effect synchronized with the values used by its setup logic and expose stale-closure bugs instead of hiding them.
An Effect runs its setup function after React commits a render, and its dependency array tells React which reactive values should cause the Effect to be re-synchronized. Reactive values include props, state, and variables or functions declared inside the component body. If the Effect reads one of these values, that value generally belongs in the dependency list.
The lint rule analyzes the Effect's code and warns when a referenced reactive value is omitted. For example, an Effect that reads `roomId` but declares `[]` can continue using an older `roomId` captured by its closure. Adding `[roomId]` makes React re-run the Effect when that value changes.
The right fix is usually not to silence the warning. Instead, restructure the Effect so its dependencies represent what the Effect actually needs. Common fixes include moving object or function creation inside the Effect, deriving values during render rather than storing them in an Effect, or using a stable value when that is the intended design.
Some values are intentionally stable and do not need to appear as changing dependencies. For example, React state setter functions have stable identity, and a `ref` object's identity remains stable across renders. By contrast, an object, array, or function created during rendering normally gets a new identity on each render, so using it as a dependency can cause repeated synchronization unless its creation is moved or otherwise stabilized.
Key points
- List every reactive value read by an Effect unless it is a value whose identity React guarantees to be stable.
- Missing dependencies can create stale closures because the Effect may retain values from an older render.
- Do not treat the dependency array as a scheduling preference; it describes the values the Effect depends on.
- If adding a dependency causes excessive re-runs, restructure the Effect rather than suppressing the lint warning.
- Functions and objects created during render can change identity on every render and therefore affect dependency behavior.
Interview questions
What problem does the exhaustive-deps rule solve?
It checks that an Effect's dependency array includes the reactive values that the Effect reads. Without a needed dependency, the Effect can keep using a value captured by an older render. That is a stale-closure problem. The rule therefore helps keep the Effect synchronized with the component's current inputs.
Why can adding a missing dependency change how often an Effect runs?
React compares dependency values between renders and re-synchronizes the Effect when a dependency changes. If the dependency is an object or function created during rendering, its identity can change on every render. That can make the Effect run more often than intended. The usual solution is to restructure the code or stabilize the relevant value when that stability is semantically appropriate.
Why shouldn't you simply disable exhaustive-deps when it reports a warning?
The warning often indicates that the Effect's synchronization logic does not match the values it uses. Disabling it can hide a stale-closure bug and make future changes harder to reason about. A better approach is to identify why the dependency is changing or why the Effect needs that value. Then restructure the Effect so its dependencies accurately describe its synchronization requirements.
What is the difference between a reactive value and a stable value in dependency analysis?
A reactive value can change as a result of rendering, such as a prop, state value, or variable declared in the component body. A stable value has an identity that React guarantees not to change across renders, such as a state setter or the ref object returned by `useRef`. Effects generally need to declare reactive values they read, while stable values do not create changing synchronization requirements. The important distinction is identity and whether the value can vary between renders.
An Effect keeps reconnecting because it depends on a function declared inside the component. How would you investigate and fix it?
First, I would verify whether the Effect genuinely needs that function and whether the function captures reactive values. A function declared during rendering normally has a new identity on each render, so it can cause the dependency to change. If the function is only needed by the Effect, I would usually move its definition into the Effect. If it is shared elsewhere and stable identity is actually required, `useCallback` may be appropriate, with its own dependencies declared correctly.