[compiler] Allow nonreactive stable types as extraneous deps (#35185)
When checking ValidateExhaustiveDeps internally, this seems to be the most common case that it flags. The current exhaustive-deps rule allows extraneous deps if they are a set of stable types. So here we reuse our existing isStableType() util in the compiler to allow this case. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35185). * #35201 * #35202 * #35192 * #35190 * #35186 * __->__ #35185
Joseph Savona committed
Nov 24, 2025 at 12:15 UTC
c9a8cf3411baed43d4e24fab6ec895768b297fd2
3 files changed
+156
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts
+14
@@ -261,6 +261,20 @@ export function validateExhaustiveDependencies(
261
extra.push(dep);
262
}
263
264
+ /*
265
+ * For compatiblity with the existing exhaustive-deps rule, we allow
266
+ * known-stable values as dependencies even if the value is not reactive.
267
+ * This allows code that takes a dep on a non-reactive setState function
268
+ * to pass, for example.
269
+ */
270
+ retainWhere(extra, dep => {
271
+ const isNonReactiveStableValue =
272
+ dep.root.kind === 'NamedLocal' &&
273
+ !dep.root.value.reactive &&
274
+ isStableType(dep.root.value.identifier);
275
+ return !isNonReactiveStableValue;
276
+ });
277
+
278
if (missing.length !== 0 || extra.length !== 0) {
279
let suggestions: Array<CompilerSuggestion> | null = null;
280
if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md
new
+100
@@ -0,0 +1,100 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @validateExhaustiveMemoizationDependencies
6
+import {
7
+ useCallback,
8
+ useTransition,
9
+ useState,
10
+ useOptimistic,
11
+ useActionState,
12
+ useRef,
13
+ useReducer,
14
+} from 'react';
15
+
16
+function useFoo() {
17
+ const [s, setState] = useState();
18
+ const ref = useRef(null);
19
+ const [t, startTransition] = useTransition();
20
+ const [u, addOptimistic] = useOptimistic();
21
+ const [v, dispatch] = useReducer(() => {}, null);
22
+ const [isPending, dispatchAction] = useActionState(() => {}, null);
23
+
24
+ return useCallback(() => {
25
+ dispatch();
26
+ startTransition(() => {});
27
+ addOptimistic();
28
+ setState(null);
29
+ dispatchAction();
30
+ ref.current = true;
31
+ }, [
32
+ // intentionally adding unnecessary deps on nonreactive stable values
33
+ // to check that they're allowed
34
+ dispatch,
35
+ startTransition,
36
+ addOptimistic,
37
+ setState,
38
+ dispatchAction,
39
+ ref,
40
+ ]);
41
+}
42
+
43
+export const FIXTURE_ENTRYPOINT = {
44
+ fn: useFoo,
45
+ params: [],
46
+};
47
+
48
+```
49
+
50
+## Code
51
+
52
+```javascript
53
+import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
54
+import {
55
+ useCallback,
56
+ useTransition,
57
+ useState,
58
+ useOptimistic,
59
+ useActionState,
60
+ useRef,
61
+ useReducer,
62
+} from "react";
63
+
64
+function useFoo() {
65
+ const $ = _c(1);
66
+ const [, setState] = useState();
67
+ const ref = useRef(null);
68
+ const [, startTransition] = useTransition();
69
+ const [, addOptimistic] = useOptimistic();
70
+ const [, dispatch] = useReducer(_temp, null);
71
+ const [, dispatchAction] = useActionState(_temp2, null);
72
+ let t0;
73
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
74
+ t0 = () => {
75
+ dispatch();
76
+ startTransition(_temp3);
77
+ addOptimistic();
78
+ setState(null);
79
+ dispatchAction();
80
+ ref.current = true;
81
+ };
82
+ $[0] = t0;
83
+ } else {
84
+ t0 = $[0];
85
+ }
86
+ return t0;
87
+}
88
+function _temp3() {}
89
+function _temp2() {}
90
+function _temp() {}
91
+
92
+export const FIXTURE_ENTRYPOINT = {
93
+ fn: useFoo,
94
+ params: [],
95
+};
96
+
97
+```
98
+
99
+### Eval output
100
+(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js
new
+42
@@ -0,0 +1,42 @@
1
+// @validateExhaustiveMemoizationDependencies
2
+import {
3
+ useCallback,
4
+ useTransition,
5
+ useState,
6
+ useOptimistic,
7
+ useActionState,
8
+ useRef,
9
+ useReducer,
10
+} from 'react';
11
+
12
+function useFoo() {
13
+ const [s, setState] = useState();
14
+ const ref = useRef(null);
15
+ const [t, startTransition] = useTransition();
16
+ const [u, addOptimistic] = useOptimistic();
17
+ const [v, dispatch] = useReducer(() => {}, null);
18
+ const [isPending, dispatchAction] = useActionState(() => {}, null);
19
+
20
+ return useCallback(() => {
21
+ dispatch();
22
+ startTransition(() => {});
23
+ addOptimistic();
24
+ setState(null);
25
+ dispatchAction();
26
+ ref.current = true;
27
+ }, [
28
+ // intentionally adding unnecessary deps on nonreactive stable values
29
+ // to check that they're allowed
30
+ dispatch,
31
+ startTransition,
32
+ addOptimistic,
33
+ setState,
34
+ dispatchAction,
35
+ ref,
36
+ ]);
37
+}
38
+
39
+export const FIXTURE_ENTRYPOINT = {
40
+ fn: useFoo,
41
+ params: [],
42
+};