@samitouri / QOS-React-2 / commits / 704aeed022

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