Skip to content

Commit 67c1487

Browse files
authored
[compiler] Allow extraneous non-reactive locals (facebook#35190)
The existing exhaustive-deps rule allows omitting non-reactive dependencies, even if they're not memoized. Conceptually, if a value is non-reactive then it cannot semantically change. Even if the value is a new object, that object represents the exact same value and doesn't necessitate redoing downstream computation. Thus its fine to exclude nonreactive dependencies, whether they're a stable type or not. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35190). * facebook#35201 * facebook#35202 * facebook#35192 * __->__ facebook#35190
1 parent 454e01e commit 67c1487

File tree

3 files changed

+101
-58
lines changed

3 files changed

+101
-58
lines changed

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts

Lines changed: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,37 @@ import {retainWhere} from '../Utils/utils';
4343
const DEBUG = false;
4444

4545
/**
46-
* Validates that existing manual memoization had exhaustive dependencies.
47-
* Memoization with missing or extra reactive dependencies is invalid React
48-
* and compilation can change behavior, causing a value to be computed more
49-
* or less times.
46+
* Validates that existing manual memoization is exhaustive and does not
47+
* have extraneous dependencies. The goal of the validation is to ensure
48+
* that auto-memoization will not substantially change the behavior of
49+
* the program:
50+
* - If the manual dependencies were non-exhaustive (missing important deps)
51+
* then auto-memoization will include those dependencies, and cause the
52+
* value to update *more* frequently.
53+
* - If the manual dependencies had extraneous deps, then auto memoization
54+
* will remove them and cause the value to update *less* frequently.
5055
*
51-
* TODOs:
52-
* - Handle cases of mixed optional and non-optional versions of the same path,
53-
* eg referecing both x.y.z and x.y?.z in the same memo block. we should collapse
54-
* this into a single canonical dep that we look for in the manual deps. see the
55-
* existing exhaustive deps rule for implementation.
56-
* - Handle cases where the user deps were not simple identifiers + property chains.
57-
* We try to detect this in ValidateUseMemo but we miss some cases. The problem
58-
* is that invalid forms can be value blocks or function calls that don't get
59-
* removed by DCE, leaving a structure like:
56+
* We consider a value V as missing if ALL of the following conditions are met:
57+
* - V is reactive
58+
* - There is no manual dependency path P such that whenever V would change,
59+
* P would also change. If V is `x.y.z`, this means there must be some
60+
* path P that is either `x.y.z`, `x.y`, or `x`. Note that we assume no
61+
* interior mutability, such that a shorter path "covers" changes to longer
62+
* more precise paths.
63+
*
64+
* We consider a value V extraneous if either of the folowing are true:
65+
* - V is a reactive local that is unreferenced
66+
* - V is a global that is unreferenced
67+
*
68+
* In other words, we allow extraneous non-reactive values since we know they cannot
69+
* impact how often the memoization would run.
70+
*
71+
* ## TODO: Invalid, Complex Deps
72+
*
73+
* Handle cases where the user deps were not simple identifiers + property chains.
74+
* We try to detect this in ValidateUseMemo but we miss some cases. The problem
75+
* is that invalid forms can be value blocks or function calls that don't get
76+
* removed by DCE, leaving a structure like:
6077
*
6178
* StartMemoize
6279
* t0 = <value to memoize>
@@ -209,31 +226,9 @@ export function validateExhaustiveDependencies(
209226
reason: 'Unexpected function dependency',
210227
loc: value.loc,
211228
});
212-
/**
213-
* Dependencies technically only need to include reactive values. However,
214-
* reactivity inference for general values is subtle since it involves all
215-
* of our complex control and data flow analysis. To keep results more
216-
* stable and predictable to developers, we intentionally stay closer to
217-
* the rules of the classic exhaustive-deps rule. Values should be included
218-
* as dependencies if either of the following is true:
219-
* - They're reactive
220-
* - They're non-reactive and not a known-stable value type.
221-
*
222-
* Thus `const ref: Ref = cond ? ref1 : ref2` has to be a dependency
223-
* (assuming `cond` is reactive) since it's reactive despite being a ref.
224-
*
225-
* Similarly, `const x = [1,2,3]` has to be a dependency since even
226-
* though it's non reactive, it's not a known stable type.
227-
*
228-
* TODO: consider reimplementing a simpler form of reactivity inference.
229-
* Ideally we'd consider `const ref: Ref = cond ? ref1 : ref2` as a required
230-
* dependency even if our data/control flow tells us that `cond` is non-reactive.
231-
* It's simpler for developers to reason about based on a more structural/AST
232-
* driven approach.
233-
*/
234-
const isRequiredDependency =
235-
reactive.has(inferredDependency.identifier.id) ||
236-
!isStableType(inferredDependency.identifier);
229+
const isRequiredDependency = reactive.has(
230+
inferredDependency.identifier.id,
231+
);
237232
let hasMatchingManualDependency = false;
238233
for (const manualDependency of manualDependencies) {
239234
if (
@@ -265,18 +260,13 @@ export function validateExhaustiveDependencies(
265260
extra.push(dep);
266261
}
267262

268-
/*
269-
* For compatiblity with the existing exhaustive-deps rule, we allow
270-
* known-stable values as dependencies even if the value is not reactive.
271-
* This allows code that takes a dep on a non-reactive setState function
272-
* to pass, for example.
263+
/**
264+
* Per docblock, we only consider dependencies as extraneous if
265+
* they are unused globals or reactive locals. Notably, this allows
266+
* non-reactive locals.
273267
*/
274268
retainWhere(extra, dep => {
275-
const isNonReactiveStableValue =
276-
dep.root.kind === 'NamedLocal' &&
277-
!dep.root.value.reactive &&
278-
isStableType(dep.root.value.identifier);
279-
return !isNonReactiveStableValue;
269+
return dep.root.kind === 'Global' || dep.root.value.reactive;
280270
});
281271

282272
if (missing.length !== 0 || extra.length !== 0) {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps.expect.md

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
```javascript
55
// @validateExhaustiveMemoizationDependencies
6-
import {useMemo} from 'react';
6+
import {useCallback, useMemo} from 'react';
77
import {makeObject_Primitives, Stringify} from 'shared-runtime';
88

99
function useHook1(x) {
@@ -47,14 +47,25 @@ function useHook6(x) {
4747
}, [x]);
4848
}
4949

50+
function useHook7(x) {
51+
const [state, setState] = useState(true);
52+
const f = () => {
53+
setState(x => !x);
54+
};
55+
return useCallback(() => {
56+
f();
57+
}, [f]);
58+
}
59+
5060
function Component({x, y, z}) {
5161
const a = useHook1(x);
5262
const b = useHook2(x);
5363
const c = useHook3(x);
5464
const d = useHook4(x, y, z);
5565
const e = useHook5(x);
5666
const f = useHook6(x);
57-
return <Stringify results={[a, b, c, d, e, f]} />;
67+
const g = useHook7(x);
68+
return <Stringify results={[a, b, c, d, e, f, g]} />;
5869
}
5970

6071
```
@@ -63,7 +74,7 @@ function Component({x, y, z}) {
6374
6475
```javascript
6576
import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies
66-
import { useMemo } from "react";
77+
import { useCallback, useMemo } from "react";
6778
import { makeObject_Primitives, Stringify } from "shared-runtime";
6879

6980
function useHook1(x) {
@@ -121,34 +132,65 @@ function useHook6(x) {
121132
return f;
122133
}
123134

135+
function useHook7(x) {
136+
const $ = _c(2);
137+
const [, setState] = useState(true);
138+
let t0;
139+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
140+
t0 = () => {
141+
setState(_temp);
142+
};
143+
$[0] = t0;
144+
} else {
145+
t0 = $[0];
146+
}
147+
const f = t0;
148+
let t1;
149+
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
150+
t1 = () => {
151+
f();
152+
};
153+
$[1] = t1;
154+
} else {
155+
t1 = $[1];
156+
}
157+
return t1;
158+
}
159+
function _temp(x_0) {
160+
return !x_0;
161+
}
162+
124163
function Component(t0) {
125-
const $ = _c(7);
164+
const $ = _c(8);
126165
const { x, y, z } = t0;
127166
const a = useHook1(x);
128167
const b = useHook2(x);
129168
const c = useHook3(x);
130169
const d = useHook4(x, y, z);
131170
const e = useHook5(x);
132171
const f = useHook6(x);
172+
const g = useHook7(x);
133173
let t1;
134174
if (
135175
$[0] !== a ||
136176
$[1] !== b ||
137177
$[2] !== c ||
138178
$[3] !== d ||
139179
$[4] !== e ||
140-
$[5] !== f
180+
$[5] !== f ||
181+
$[6] !== g
141182
) {
142-
t1 = <Stringify results={[a, b, c, d, e, f]} />;
183+
t1 = <Stringify results={[a, b, c, d, e, f, g]} />;
143184
$[0] = a;
144185
$[1] = b;
145186
$[2] = c;
146187
$[3] = d;
147188
$[4] = e;
148189
$[5] = f;
149-
$[6] = t1;
190+
$[6] = g;
191+
$[7] = t1;
150192
} else {
151-
t1 = $[6];
193+
t1 = $[7];
152194
}
153195
return t1;
154196
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @validateExhaustiveMemoizationDependencies
2-
import {useMemo} from 'react';
2+
import {useCallback, useMemo} from 'react';
33
import {makeObject_Primitives, Stringify} from 'shared-runtime';
44

55
function useHook1(x) {
@@ -43,12 +43,23 @@ function useHook6(x) {
4343
}, [x]);
4444
}
4545

46+
function useHook7(x) {
47+
const [state, setState] = useState(true);
48+
const f = () => {
49+
setState(x => !x);
50+
};
51+
return useCallback(() => {
52+
f();
53+
}, [f]);
54+
}
55+
4656
function Component({x, y, z}) {
4757
const a = useHook1(x);
4858
const b = useHook2(x);
4959
const c = useHook3(x);
5060
const d = useHook4(x, y, z);
5161
const e = useHook5(x);
5262
const f = useHook6(x);
53-
return <Stringify results={[a, b, c, d, e, f]} />;
63+
const g = useHook7(x);
64+
return <Stringify results={[a, b, c, d, e, f, g]} />;
5465
}

0 commit comments

Comments
 (0)