Skip to content

Commit d72dce3

Browse files
committed
docs: Add detailed Stale Closure warnings to Hooks
1 parent 47e64bf commit d72dce3

4 files changed

Lines changed: 38 additions & 1 deletion

File tree

src/content/reference/eslint-plugin-react-hooks/lints/exhaustive-deps.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@ Validates that dependency arrays for React hooks contain all necessary dependenc
1010

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

13-
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.
13+
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:
14+
15+
- Event handlers or effects reading old state/props.
16+
- Effects that should run on state changes never re‑executing.
17+
- Callbacks that always operate on stale data.
18+
19+
Currently, the `eslint-plugin-react-hooks` provides a static analysis rule (`exhaustive-deps`) that warns about missing dependencies. However, this rule:
20+
21+
- Cannot catch all cases (e.g., dynamic values, complex control flow, or custom hooks that obscure dependencies).
22+
- Only runs at lint time; developers may ignore or disable the warning.
23+
- Does not warn about actual runtime behavior—it cannot tell if a stale closure actually read an outdated value during a particular render.
24+
25+
As a result, developers often spend hours chasing down stale‑closure bugs, especially in larger codebases or when integrating third‑party hooks.
1426

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

src/content/reference/react/useCallback.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ export default function ProductPage({ productId, referrer, theme }) {
4848
4949
* `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.
5050
51+
<Pitfall>
52+
53+
**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.
54+
55+
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!
56+
57+
</Pitfall>
58+
5159
#### Returns {/*returns*/}
5260
5361
On the initial render, `useCallback` returns the `fn` function you have passed.

src/content/reference/react/useEffect.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,6 +1141,15 @@ useEffect(() => {
11411141
11421142
</Pitfall>
11431143
1144+
<Pitfall>
1145+
1146+
**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.
1147+
1148+
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!
1149+
1150+
</Pitfall>
1151+
1152+
11441153
<Recipes titleText="Examples of passing reactive dependencies" titleId="examples-dependencies">
11451154
11461155
#### Passing a dependency array {/*passing-a-dependency-array*/}

src/content/reference/react/useMemo.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ function TodoList({ todos, tab }) {
4848

4949
* `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.
5050

51+
<Pitfall>
52+
53+
**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.
54+
55+
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!
56+
57+
</Pitfall>
58+
5159
#### Returns {/*returns*/}
5260

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

0 commit comments

Comments
 (0)