@samitouri / QOS-React-2 / commits / c9a8cf3411

[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 +};