Skip to content

Commit 704aeed

Browse files
authored
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.
1 parent 3730b40 commit 704aeed

File tree

10 files changed

+169
-2
lines changed

10 files changed

+169
-2
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
BuiltInUseInsertionEffectHookId,
1414
BuiltInUseLayoutEffectHookId,
1515
BuiltInUseOperatorId,
16+
BuiltInUseReducerId,
1617
BuiltInUseRefId,
1718
BuiltInUseStateId,
1819
ShapeRegistry,
@@ -265,6 +266,18 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
265266
returnValueReason: ValueReason.State,
266267
}),
267268
],
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+
],
268281
[
269282
"useRef",
270283
addHook(DEFAULT_SHAPES, {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,11 @@ export enum ValueReason {
12541254
*/
12551255
State = "state",
12561256

1257+
/**
1258+
* A value returned from `useReducer`
1259+
*/
1260+
ReducerState = "reducer-state",
1261+
12571262
/**
12581263
* Props of a component or arguments of a hook.
12591264
*/
@@ -1493,6 +1498,14 @@ export function isSetStateType(id: Identifier): boolean {
14931498
return id.type.kind === "Function" && id.type.shapeId === "BuiltInSetState";
14941499
}
14951500

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+
14961509
export function isUseEffectHookType(id: Identifier): boolean {
14971510
return (
14981511
id.type.kind === "Function" && id.type.shapeId === "BuiltInUseEffectHook"

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ function addShape(
118118
export type HookKind =
119119
| "useContext"
120120
| "useState"
121+
| "useReducer"
121122
| "useRef"
122123
| "useEffect"
123124
| "useLayoutEffect"
@@ -200,6 +201,8 @@ export const BuiltInUseEffectHookId = "BuiltInUseEffectHook";
200201
export const BuiltInUseLayoutEffectHookId = "BuiltInUseLayoutEffectHook";
201202
export const BuiltInUseInsertionEffectHookId = "BuiltInUseInsertionEffectHook";
202203
export const BuiltInUseOperatorId = "BuiltInUseOperator";
204+
export const BuiltInUseReducerId = "BuiltInUseReducer";
205+
export const BuiltInDispatchId = "BuiltInDispatch";
203206

204207
// ShapeRegistry with default definitions for built-ins.
205208
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
@@ -387,6 +390,25 @@ addObject(BUILTIN_SHAPES, BuiltInUseStateId, [
387390
],
388391
]);
389392

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+
390412
addObject(BUILTIN_SHAPES, BuiltInUseRefId, [
391413
["current", { kind: "Object", shapeId: BuiltInRefValueId }],
392414
]);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Place,
1616
computePostDominatorTree,
1717
getHookKind,
18+
isDispatcherType,
1819
isSetStateType,
1920
isUseOperator,
2021
} from "../HIR";
@@ -219,7 +220,10 @@ export function inferReactivePlaces(fn: HIRFunction): void {
219220

220221
if (hasReactiveInput) {
221222
for (const lvalue of eachInstructionLValue(instruction)) {
222-
if (isSetStateType(lvalue.identifier)) {
223+
if (
224+
isSetStateType(lvalue.identifier) ||
225+
isDispatcherType(lvalue.identifier)
226+
) {
223227
continue;
224228
}
225229
reactiveIdentifiers.markReactive(lvalue);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2117,6 +2117,8 @@ function getWriteErrorReason(abstractValue: AbstractValue): string {
21172117
return "Mutating component props or hook arguments is not allowed. Consider using a local variable instead";
21182118
} else if (abstractValue.reason.has(ValueReason.State)) {
21192119
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";
21202122
} else {
21212123
return "This mutates a variable that React considers immutable";
21222124
}

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonReactiveDependencies.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ReactiveFunction,
1111
ReactiveInstruction,
1212
ReactiveScopeBlock,
13+
isDispatcherType,
1314
isSetStateType,
1415
} from "../HIR";
1516
import { eachPatternOperand } from "../HIR/visitors";
@@ -56,7 +57,10 @@ class Visitor extends ReactiveFunctionVisitor<ReactiveIdentifiers> {
5657
case "Destructure": {
5758
if (state.has(value.value.identifier.id)) {
5859
for (const lvalue of eachPatternOperand(value.lvalue.pattern)) {
59-
if (isSetStateType(lvalue.identifier)) {
60+
if (
61+
isSetStateType(lvalue.identifier) ||
62+
isDispatcherType(lvalue.identifier)
63+
) {
6064
continue;
6165
}
6266
state.add(lvalue.identifier.id);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};

0 commit comments

Comments
 (0)