Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,19 @@ Validates that dependency arrays for React hooks contain all necessary dependenc

## Rule Details {/*rule-details*/}

React hooks like `useEffect`, `useMemo`, and `useCallback` accept dependency arrays. When a value referenced inside these hooks isn't included in the dependency array, React won't re-run the effect or recalculate the value when that dependency changes. This causes stale closures where the hook uses outdated values.
React hooks that accept dependency arrays (`useEffect`, `useCallback`, `useMemo`) rely on developers to manually list all reactive values that the hook's closure depends on. When a value is omitted from the dependencies array, the hook's closure continues to reference an outdated value from a previous render—a "stale closure". This leads to subtle, difficult‑to‑debug bugs such as:

- Event handlers or effects reading old state/props.
- Effects that should run on state changes never re‑executing.
- Callbacks that always operate on stale data.

Currently, the `eslint-plugin-react-hooks` provides a static analysis rule (`exhaustive-deps`) that warns about missing dependencies. However, this rule:

- Cannot catch all cases (e.g., dynamic values, complex control flow, or custom hooks that obscure dependencies).
- Only runs at lint time; developers may ignore or disable the warning.
- Does not warn about actual runtime behavior—it cannot tell if a stale closure actually read an outdated value during a particular render.

As a result, developers often spend hours chasing down stale‑closure bugs, especially in larger codebases or when integrating third‑party hooks.

## Common Violations {/*common-violations*/}

Expand Down
8 changes: 8 additions & 0 deletions src/content/reference/react/useCallback.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ export default function ProductPage({ productId, referrer, theme }) {

* `dependencies`: The list of all reactive values referenced inside of the `fn` code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is [configured for React](/learn/editor-setup#linting), it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like `[dep1, dep2, dep3]`. React will compare each dependency with its previous value using the [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) comparison algorithm.

<Pitfall>

**If you omit a dependency from the array, your cached function will capture the "stale" value from the render when it was created.** This creates a "stale closure" bug where your callback operates on outdated state or props.

Because the `eslint-plugin-react-hooks` relies on static analysis, it cannot catch every missing dependency or warn you at runtime when a stale value is read. Always ensure every reactive value used in the callback is listed to avoid subtle, hard-to-debug issues!

</Pitfall>

#### Returns {/*returns*/}

On the initial render, `useCallback` returns the `fn` function you have passed.
Expand Down
9 changes: 9 additions & 0 deletions src/content/reference/react/useEffect.md
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,15 @@ useEffect(() => {

</Pitfall>

<Pitfall>

**If you omit a dependency from the array, your Effect's closure will capture the "stale" value from the render when it was created.** This creates a "stale closure" bug where your Effect sees outdated state or props.

Because the `eslint-plugin-react-hooks` relies on static analysis, it cannot catch every missing dependency or warn you at runtime when a stale value is read. Always ensure every reactive value used in the Effect is listed to avoid subtle, hard-to-debug issues!

</Pitfall>


<Recipes titleText="Examples of passing reactive dependencies" titleId="examples-dependencies">

#### Passing a dependency array {/*passing-a-dependency-array*/}
Expand Down
8 changes: 8 additions & 0 deletions src/content/reference/react/useMemo.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ function TodoList({ todos, tab }) {

* `dependencies`: The list of all reactive values referenced inside of the `calculateValue` code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is [configured for React](/learn/editor-setup#linting), it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like `[dep1, dep2, dep3]`. React will compare each dependency with its previous value using the [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) comparison.

<Pitfall>

**If you omit a dependency from the array, your cached calculation will capture the "stale" value from the render when it was created.** This creates a "stale closure" bug where your memoized value is computed using outdated state or props.

Because the `eslint-plugin-react-hooks` relies on static analysis, it cannot catch every missing dependency or warn you at runtime when a stale value is read. Always ensure every reactive value used in the calculation is listed to avoid subtle, hard-to-debug issues!

</Pitfall>

#### Returns {/*returns*/}

On the initial render, `useMemo` returns the result of calling `calculateValue` with no arguments.
Expand Down
Loading