Skip to content

Commit 2d0a5e3

Browse files
authored
[compiler] Patch for reactive refs in inferred effect dependencies (#32991)
Inferred effect dependencies and inlined jsx (both experimental features) rely on `InferReactivePlaces` to determine their dependencies. Since adding type inference for phi nodes (#30796), we have been incorrectly inferring stable-typed value blocks (e.g. `props.cond ? setState1 : setState2`) as non-reactive. This fix patches InferReactivePlaces instead of adding a new pass since we want non-reactivity propagated correctly
1 parent 0c28a09 commit 2d0a5e3

File tree

5 files changed

+234
-5
lines changed

5 files changed

+234
-5
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts

+34
Original file line numberDiff line numberDiff line change
@@ -1738,6 +1738,40 @@ export function isStableType(id: Identifier): boolean {
17381738
);
17391739
}
17401740

1741+
export function isStableTypeContainer(id: Identifier): boolean {
1742+
const type_ = id.type;
1743+
if (type_.kind !== 'Object') {
1744+
return false;
1745+
}
1746+
return (
1747+
isUseStateType(id) || // setState
1748+
type_.shapeId === 'BuiltInUseActionState' || // setActionState
1749+
isUseReducerType(id) || // dispatcher
1750+
type_.shapeId === 'BuiltInUseTransition' // startTransition
1751+
);
1752+
}
1753+
1754+
export function evaluatesToStableTypeOrContainer(
1755+
env: Environment,
1756+
{value}: Instruction,
1757+
): boolean {
1758+
if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
1759+
const callee =
1760+
value.kind === 'CallExpression' ? value.callee : value.property;
1761+
1762+
const calleeHookKind = getHookKind(env, callee.identifier);
1763+
switch (calleeHookKind) {
1764+
case 'useState':
1765+
case 'useReducer':
1766+
case 'useActionState':
1767+
case 'useRef':
1768+
case 'useTransition':
1769+
return true;
1770+
}
1771+
}
1772+
return false;
1773+
}
1774+
17411775
export function isUseEffectHookType(id: Identifier): boolean {
17421776
return (
17431777
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectHook'

compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts

+111-1
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@ import {CompilerError} from '..';
99
import {
1010
BlockId,
1111
Effect,
12+
Environment,
1213
HIRFunction,
1314
Identifier,
1415
IdentifierId,
16+
Instruction,
1517
Place,
1618
computePostDominatorTree,
19+
evaluatesToStableTypeOrContainer,
1720
getHookKind,
1821
isStableType,
22+
isStableTypeContainer,
1923
isUseOperator,
24+
isUseRefType,
2025
} from '../HIR';
2126
import {PostDominator} from '../HIR/Dominator';
2227
import {
@@ -31,6 +36,103 @@ import {
3136
import DisjointSet from '../Utils/DisjointSet';
3237
import {assertExhaustive} from '../Utils/utils';
3338

39+
/**
40+
* Side map to track and propagate sources of stability (i.e. hook calls such as
41+
* `useRef()` and property reads such as `useState()[1]). Note that this
42+
* requires forward data flow analysis since stability is not part of React
43+
* Compiler's type system.
44+
*/
45+
class StableSidemap {
46+
map: Map<IdentifierId, {isStable: boolean}> = new Map();
47+
env: Environment;
48+
49+
constructor(env: Environment) {
50+
this.env = env;
51+
}
52+
53+
handleInstruction(instr: Instruction): void {
54+
const {value, lvalue} = instr;
55+
56+
switch (value.kind) {
57+
case 'CallExpression':
58+
case 'MethodCall': {
59+
/**
60+
* Sources of stability are known hook calls
61+
*/
62+
if (evaluatesToStableTypeOrContainer(this.env, instr)) {
63+
if (isStableType(lvalue.identifier)) {
64+
this.map.set(lvalue.identifier.id, {
65+
isStable: true,
66+
});
67+
} else {
68+
this.map.set(lvalue.identifier.id, {
69+
isStable: false,
70+
});
71+
}
72+
} else if (
73+
this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
74+
isUseRefType(lvalue.identifier)
75+
) {
76+
this.map.set(lvalue.identifier.id, {
77+
isStable: true,
78+
});
79+
}
80+
break;
81+
}
82+
83+
case 'Destructure':
84+
case 'PropertyLoad': {
85+
/**
86+
* PropertyLoads may from stable containers may also produce stable
87+
* values. ComputedLoads are technically safe for now (as all stable
88+
* containers have differently-typed elements), but are not handled as
89+
* they should be rare anyways.
90+
*/
91+
const source =
92+
value.kind === 'Destructure'
93+
? value.value.identifier.id
94+
: value.object.identifier.id;
95+
const entry = this.map.get(source);
96+
if (entry) {
97+
for (const lvalue of eachInstructionLValue(instr)) {
98+
if (isStableTypeContainer(lvalue.identifier)) {
99+
this.map.set(lvalue.identifier.id, {
100+
isStable: false,
101+
});
102+
} else if (isStableType(lvalue.identifier)) {
103+
this.map.set(lvalue.identifier.id, {
104+
isStable: true,
105+
});
106+
}
107+
}
108+
}
109+
break;
110+
}
111+
112+
case 'StoreLocal': {
113+
const entry = this.map.get(value.value.identifier.id);
114+
if (entry) {
115+
this.map.set(lvalue.identifier.id, entry);
116+
this.map.set(value.lvalue.place.identifier.id, entry);
117+
}
118+
break;
119+
}
120+
121+
case 'LoadLocal': {
122+
const entry = this.map.get(value.place.identifier.id);
123+
if (entry) {
124+
this.map.set(lvalue.identifier.id, entry);
125+
}
126+
break;
127+
}
128+
}
129+
}
130+
131+
isStable(id: IdentifierId): boolean {
132+
const entry = this.map.get(id);
133+
return entry != null ? entry.isStable : false;
134+
}
135+
}
34136
/*
35137
* Infers which `Place`s are reactive, ie may *semantically* change
36138
* over the course of the component/hook's lifetime. Places are reactive
@@ -111,6 +213,7 @@ import {assertExhaustive} from '../Utils/utils';
111213
*/
112214
export function inferReactivePlaces(fn: HIRFunction): void {
113215
const reactiveIdentifiers = new ReactivityMap(findDisjointMutableValues(fn));
216+
const stableIdentifierSources = new StableSidemap(fn.env);
114217
for (const param of fn.params) {
115218
const place = param.kind === 'Identifier' ? param : param.place;
116219
reactiveIdentifiers.markReactive(place);
@@ -184,6 +287,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
184287
}
185288
}
186289
for (const instruction of block.instructions) {
290+
stableIdentifierSources.handleInstruction(instruction);
187291
const {value} = instruction;
188292
let hasReactiveInput = false;
189293
/*
@@ -218,7 +322,13 @@ export function inferReactivePlaces(fn: HIRFunction): void {
218322

219323
if (hasReactiveInput) {
220324
for (const lvalue of eachInstructionLValue(instruction)) {
221-
if (isStableType(lvalue.identifier)) {
325+
/**
326+
* Note that it's not correct to mark all stable-typed identifiers
327+
* as non-reactive, since ternaries and other value blocks can
328+
* produce reactive identifiers typed as these.
329+
* (e.g. `props.cond ? setState1 : setState2`)
330+
*/
331+
if (stableIdentifierSources.isStable(lvalue.identifier.id)) {
222332
continue;
223333
}
224334
reactiveIdentifiers.markReactive(lvalue);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @inferEffectDependencies
6+
import {useRef, useEffect} from 'react';
7+
import {print, mutate} from 'shared-runtime';
8+
9+
function Component({cond}) {
10+
const arr = useRef([]);
11+
const other = useRef([]);
12+
// Although arr and other are both stable, derived is not
13+
const derived = cond ? arr : other;
14+
useEffect(() => {
15+
mutate(derived.current);
16+
print(derived.current);
17+
});
18+
return arr;
19+
}
20+
21+
```
22+
23+
## Code
24+
25+
```javascript
26+
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
27+
import { useRef, useEffect } from "react";
28+
import { print, mutate } from "shared-runtime";
29+
30+
function Component(t0) {
31+
const $ = _c(4);
32+
const { cond } = t0;
33+
let t1;
34+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
35+
t1 = [];
36+
$[0] = t1;
37+
} else {
38+
t1 = $[0];
39+
}
40+
const arr = useRef(t1);
41+
let t2;
42+
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
43+
t2 = [];
44+
$[1] = t2;
45+
} else {
46+
t2 = $[1];
47+
}
48+
const other = useRef(t2);
49+
50+
const derived = cond ? arr : other;
51+
let t3;
52+
if ($[2] !== derived) {
53+
t3 = () => {
54+
mutate(derived.current);
55+
print(derived.current);
56+
};
57+
$[2] = derived;
58+
$[3] = t3;
59+
} else {
60+
t3 = $[3];
61+
}
62+
useEffect(t3, [derived]);
63+
return arr;
64+
}
65+
66+
```
67+
68+
### Eval output
69+
(kind: exception) Fixture not implemented
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// @inferEffectDependencies
2+
import {useRef, useEffect} from 'react';
3+
import {print, mutate} from 'shared-runtime';
4+
5+
function Component({cond}) {
6+
const arr = useRef([]);
7+
const other = useRef([]);
8+
// Although arr and other are both stable, derived is not
9+
const derived = cond ? arr : other;
10+
useEffect(() => {
11+
mutate(derived.current);
12+
print(derived.current);
13+
});
14+
return arr;
15+
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ export const FIXTURE_ENTRYPOINT = {
8383
import { c as _c2 } from "react/compiler-runtime"; // @inlineJsxTransform
8484

8585
function Parent(t0) {
86-
const $ = _c2(2);
86+
const $ = _c2(3);
8787
const { children, ref } = t0;
8888
let t1;
89-
if ($[0] !== children) {
89+
if ($[0] !== children || $[1] !== ref) {
9090
if (DEV) {
9191
t1 = <div ref={ref}>{children}</div>;
9292
} else {
@@ -99,9 +99,10 @@ function Parent(t0) {
9999
};
100100
}
101101
$[0] = children;
102-
$[1] = t1;
102+
$[1] = ref;
103+
$[2] = t1;
103104
} else {
104-
t1 = $[1];
105+
t1 = $[2];
105106
}
106107
return t1;
107108
}

0 commit comments

Comments
 (0)