From d72dce3de3192373b144889ff148aa25383fba8c Mon Sep 17 00:00:00 2001 From: Aryan Chauhan Date: Sat, 23 May 2026 13:15:23 +0530 Subject: [PATCH] docs: Add detailed Stale Closure warnings to Hooks --- .../lints/exhaustive-deps.md | 14 +++++++++++++- src/content/reference/react/useCallback.md | 8 ++++++++ src/content/reference/react/useEffect.md | 9 +++++++++ src/content/reference/react/useMemo.md | 8 ++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/content/reference/eslint-plugin-react-hooks/lints/exhaustive-deps.md b/src/content/reference/eslint-plugin-react-hooks/lints/exhaustive-deps.md index daa7db6a813..53910ff5b9b 100644 --- a/src/content/reference/eslint-plugin-react-hooks/lints/exhaustive-deps.md +++ b/src/content/reference/eslint-plugin-react-hooks/lints/exhaustive-deps.md @@ -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*/} diff --git a/src/content/reference/react/useCallback.md b/src/content/reference/react/useCallback.md index cb5a3454e76..09c43dd6cdb 100644 --- a/src/content/reference/react/useCallback.md +++ b/src/content/reference/react/useCallback.md @@ -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. + + +**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! + + + #### Returns {/*returns*/} On the initial render, `useCallback` returns the `fn` function you have passed. diff --git a/src/content/reference/react/useEffect.md b/src/content/reference/react/useEffect.md index 218e65ffb1d..6aa2bdd12b2 100644 --- a/src/content/reference/react/useEffect.md +++ b/src/content/reference/react/useEffect.md @@ -1141,6 +1141,15 @@ useEffect(() => { + + +**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! + + + + #### Passing a dependency array {/*passing-a-dependency-array*/} diff --git a/src/content/reference/react/useMemo.md b/src/content/reference/react/useMemo.md index e8552e1666c..61f974189a6 100644 --- a/src/content/reference/react/useMemo.md +++ b/src/content/reference/react/useMemo.md @@ -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. + + +**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! + + + #### Returns {/*returns*/} On the initial render, `useMemo` returns the result of calling `calculateValue` with no arguments.