feat<Compiler>: consider that the dispatch function from `useReducer` is non-reactive (#29705)
Summary The dispatch function from useReducer is stable, so it is also non-reactive. the related PR: #29665 the related comment: #29674 (comment) I am not sure if the location of the new test file is appropriate😅. How did you test this change? Added the specific test compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useReducer-returned-dispatcher-is-non-reactive.expect.md.
XiaoPi committed
Jun 6, 2024 at 07:51 UTC
704aeed022f4277cd5604bf6d76199a6cfe4707f
10 files changed
+169
-2
compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
+13
@@ -13,6 +13,7 @@ import {
13
BuiltInUseInsertionEffectHookId,
14
BuiltInUseLayoutEffectHookId,
15
BuiltInUseOperatorId,
16
+ BuiltInUseReducerId,
17
BuiltInUseRefId,
18
BuiltInUseStateId,
19
ShapeRegistry,
@@ -265,6 +266,18 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
266
returnValueReason: ValueReason.State,
267
}),
268
],
269
+ [
270
+ "useReducer",
271
+ addHook(DEFAULT_SHAPES, {
272
+ positionalParams: [],
273
+ restParam: Effect.Freeze,
274
+ returnType: { kind: "Object", shapeId: BuiltInUseReducerId },
275
+ calleeEffect: Effect.Read,
276
+ hookKind: "useReducer",
277
+ returnValueKind: ValueKind.Frozen,
278
+ returnValueReason: ValueReason.ReducerState,
279
+ }),
280
+ ],
281
[
282
"useRef",
283
addHook(DEFAULT_SHAPES, {
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+13
@@ -1254,6 +1254,11 @@ export enum ValueReason {
1254
*/
1255
State = "state",
1256
1257
+ /**
1258
+ * A value returned from `useReducer`
1259
+ */
1260
+ ReducerState = "reducer-state",
1261
+
1262
/**
1263
* Props of a component or arguments of a hook.
1264
*/
@@ -1493,6 +1498,14 @@ export function isSetStateType(id: Identifier): boolean {
1498
return id.type.kind === "Function" && id.type.shapeId === "BuiltInSetState";
1499
}
1500
1501
+export function isUseReducerType(id: Identifier): boolean {
1502
+ return id.type.kind === "Function" && id.type.shapeId === "BuiltInUseReducer";
1503
+}
1504
+
1505
+export function isDispatcherType(id: Identifier): boolean {
1506
+ return id.type.kind === "Function" && id.type.shapeId === "BuiltInDispatch";
1507
+}
1508
+
1509
export function isUseEffectHookType(id: Identifier): boolean {
1510
return (
1511
id.type.kind === "Function" && id.type.shapeId === "BuiltInUseEffectHook"
compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts
+22
@@ -118,6 +118,7 @@ function addShape(
118
export type HookKind =
119
| "useContext"
120
| "useState"
121
+ | "useReducer"
122
| "useRef"
123
| "useEffect"
124
| "useLayoutEffect"
@@ -200,6 +201,8 @@ export const BuiltInUseEffectHookId = "BuiltInUseEffectHook";
201
export const BuiltInUseLayoutEffectHookId = "BuiltInUseLayoutEffectHook";
202
export const BuiltInUseInsertionEffectHookId = "BuiltInUseInsertionEffectHook";
203
export const BuiltInUseOperatorId = "BuiltInUseOperator";
204
+export const BuiltInUseReducerId = "BuiltInUseReducer";
205
+export const BuiltInDispatchId = "BuiltInDispatch";
206
207
// ShapeRegistry with default definitions for built-ins.
208
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
@@ -387,6 +390,25 @@ addObject(BUILTIN_SHAPES, BuiltInUseStateId, [
390
],
391
]);
392
393
+addObject(BUILTIN_SHAPES, BuiltInUseReducerId, [
394
+ ["0", { kind: "Poly" }],
395
+ [
396
+ "1",
397
+ addFunction(
398
+ BUILTIN_SHAPES,
399
+ [],
400
+ {
401
+ positionalParams: [],
402
+ restParam: Effect.Freeze,
403
+ returnType: PRIMITIVE_TYPE,
404
+ calleeEffect: Effect.Read,
405
+ returnValueKind: ValueKind.Primitive,
406
+ },
407
+ BuiltInDispatchId
408
+ ),
409
+ ],
410
+]);
411
+
412
addObject(BUILTIN_SHAPES, BuiltInUseRefId, [
413
["current", { kind: "Object", shapeId: BuiltInRefValueId }],
414
]);
compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts
+5
-1
@@ -15,6 +15,7 @@ import {
15
Place,
16
computePostDominatorTree,
17
getHookKind,
18
+ isDispatcherType,
19
isSetStateType,
20
isUseOperator,
21
} from "../HIR";
@@ -219,7 +220,10 @@ export function inferReactivePlaces(fn: HIRFunction): void {
220
221
if (hasReactiveInput) {
222
for (const lvalue of eachInstructionLValue(instruction)) {
222
- if (isSetStateType(lvalue.identifier)) {
223
+ if (
224
+ isSetStateType(lvalue.identifier) ||
225
+ isDispatcherType(lvalue.identifier)
226
+ ) {
227
continue;
228
}
229
reactiveIdentifiers.markReactive(lvalue);
compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
+2
@@ -2117,6 +2117,8 @@ function getWriteErrorReason(abstractValue: AbstractValue): string {
2117
return "Mutating component props or hook arguments is not allowed. Consider using a local variable instead";
2118
} else if (abstractValue.reason.has(ValueReason.State)) {
2119
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
2120
+ } else if (abstractValue.reason.has(ValueReason.ReducerState)) {
2121
+ return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
2122
} else {
2123
return "This mutates a variable that React considers immutable";
2124
}
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonReactiveDependencies.ts
+5
-1
@@ -10,6 +10,7 @@ import {
10
ReactiveFunction,
11
ReactiveInstruction,
12
ReactiveScopeBlock,
13
+ isDispatcherType,
14
isSetStateType,
15
} from "../HIR";
16
import { eachPatternOperand } from "../HIR/visitors";
@@ -56,7 +57,10 @@ class Visitor extends ReactiveFunctionVisitor<ReactiveIdentifiers> {
57
case "Destructure": {
58
if (state.has(value.value.identifier.id)) {
59
for (const lvalue of eachPatternOperand(value.lvalue.pattern)) {
59
- if (isSetStateType(lvalue.identifier)) {
60
+ if (
61
+ isSetStateType(lvalue.identifier) ||
62
+ isDispatcherType(lvalue.identifier)
63
+ ) {
64
continue;
65
}
66
state.add(lvalue.identifier.id);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.modify-useReducer-state.expect.md
new
+28
@@ -0,0 +1,28 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { useReducer } from "react";
6
+
7
+function Foo() {
8
+ let [state, setState] = useReducer({ foo: 1 });
9
+ state.foo = 1;
10
+ return state;
11
+}
12
+
13
+```
14
+
15
+
16
+## Error
17
+
18
+```
19
+ 3 | function Foo() {
20
+ 4 | let [state, setState] = useReducer({ foo: 1 });
21
+> 5 | state.foo = 1;
22
+ | ^^^^^ InvalidReact: Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead (5:5)
23
+ 6 | return state;
24
+ 7 | }
25
+ 8 |
26
+```
27
+
28
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.modify-useReducer-state.js
new
+7
@@ -0,0 +1,7 @@
1
+import { useReducer } from "react";
2
+
3
+function Foo() {
4
+ let [state, setState] = useReducer({ foo: 1 });
5
+ state.foo = 1;
6
+ return state;
7
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useReducer-returned-dispatcher-is-non-reactive.expect.md
new
+57
@@ -0,0 +1,57 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { useReducer } from "react";
6
+
7
+function f() {
8
+ const [state, dispatch] = useReducer();
9
+
10
+ const onClick = () => {
11
+ dispatch();
12
+ };
13
+
14
+ return <div onClick={onClick} />;
15
+}
16
+
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: f,
19
+ params: [],
20
+ isComponent: true,
21
+};
22
+
23
+```
24
+
25
+## Code
26
+
27
+```javascript
28
+import { c as _c } from "react/compiler-runtime";
29
+import { useReducer } from "react";
30
+
31
+function f() {
32
+ const $ = _c(1);
33
+ const [state, dispatch] = useReducer();
34
+ let t0;
35
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
36
+ const onClick = () => {
37
+ dispatch();
38
+ };
39
+
40
+ t0 = <div onClick={onClick} />;
41
+ $[0] = t0;
42
+ } else {
43
+ t0 = $[0];
44
+ }
45
+ return t0;
46
+}
47
+
48
+export const FIXTURE_ENTRYPOINT = {
49
+ fn: f,
50
+ params: [],
51
+ isComponent: true,
52
+};
53
+
54
+```
55
+
56
+### Eval output
57
+(kind: ok) <div></div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useReducer-returned-dispatcher-is-non-reactive.js
new
+17
@@ -0,0 +1,17 @@
1
+import { useReducer } from "react";
2
+
3
+function f() {
4
+ const [state, dispatch] = useReducer();
5
+
6
+ const onClick = () => {
7
+ dispatch();
8
+ };
9
+
10
+ return <div onClick={onClick} />;
11
+}
12
+
13
+export const FIXTURE_ENTRYPOINT = {
14
+ fn: f,
15
+ params: [],
16
+ isComponent: true,
17
+};