Skip to content

Commit 7899729

Browse files
authored
[compiler] Option to treat "set-" prefixed callees as setState functions (#34505)
Calling setState functions during render can lead to extraneous renders or even infinite loops. We also have runtime detection for loops, but static detection is obviously even better. This PR adds an option to infer identifers as setState functions if both the following conditions are met: - The identifier is named starting with "set" - The identifier is used as the callee of a call expression By inferring values as SetState type, this allows our existing ValidateNoSetStateInRender rule to flag calls during render, disallowing examples like the following: ```js function Component({setParentState}) { setParentState(...); ^^^^^^^^^^^^^^ Error: Cannot call setState in render } ```
1 parent a51f925 commit 7899729

File tree

6 files changed

+144
-1
lines changed

6 files changed

+144
-1
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,13 @@ export const EnvironmentConfigSchema = z.object({
621621
*/
622622
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
623623

624+
/**
625+
* Treat identifiers as SetState type if both
626+
* - they are named with a "set-" prefix
627+
* - they are called somewhere
628+
*/
629+
enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),
630+
624631
/*
625632
* If specified a value, the compiler lowers any calls to `useContext` to use
626633
* this value as the callee.

compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
BuiltInObjectId,
3232
BuiltInPropsId,
3333
BuiltInRefValueId,
34+
BuiltInSetStateId,
3435
BuiltInUseRefId,
3536
} from '../HIR/ObjectShape';
3637
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
@@ -276,9 +277,16 @@ function* generateInstructionTypes(
276277
* We should change Hook to a subtype of Function or change unifier logic.
277278
* (see https://github.com/facebook/react-forget/pull/1427)
278279
*/
280+
let shapeId: string | null = null;
281+
if (env.config.enableTreatSetIdentifiersAsStateSetters) {
282+
const name = getName(names, value.callee.identifier.id);
283+
if (name.startsWith('set')) {
284+
shapeId = BuiltInSetStateId;
285+
}
286+
}
279287
yield equation(value.callee.identifier.type, {
280288
kind: 'Function',
281-
shapeId: null,
289+
shapeId,
282290
return: returnType,
283291
isConstructor: false,
284292
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
6+
function Component() {
7+
const [state, setState] = useCustomState(0);
8+
const aliased = setState;
9+
10+
setState(1);
11+
aliased(2);
12+
13+
return state;
14+
}
15+
16+
function useCustomState(init) {
17+
return useState(init);
18+
}
19+
20+
```
21+
22+
23+
## Error
24+
25+
```
26+
Found 2 errors:
27+
28+
Error: Calling setState during render may trigger an infinite loop
29+
30+
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
31+
32+
error.invalid-unconditional-set-state-hook-return-in-render.ts:6:2
33+
4 | const aliased = setState;
34+
5 |
35+
> 6 | setState(1);
36+
| ^^^^^^^^ Found setState() in render
37+
7 | aliased(2);
38+
8 |
39+
9 | return state;
40+
41+
Error: Calling setState during render may trigger an infinite loop
42+
43+
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
44+
45+
error.invalid-unconditional-set-state-hook-return-in-render.ts:7:2
46+
5 |
47+
6 | setState(1);
48+
> 7 | aliased(2);
49+
| ^^^^^^^ Found setState() in render
50+
8 |
51+
9 | return state;
52+
10 | }
53+
```
54+
55+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
2+
function Component() {
3+
const [state, setState] = useCustomState(0);
4+
const aliased = setState;
5+
6+
setState(1);
7+
aliased(2);
8+
9+
return state;
10+
}
11+
12+
function useCustomState(init) {
13+
return useState(init);
14+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
6+
function Component({setX}) {
7+
const aliased = setX;
8+
9+
setX(1);
10+
aliased(2);
11+
12+
return x;
13+
}
14+
15+
```
16+
17+
18+
## Error
19+
20+
```
21+
Found 2 errors:
22+
23+
Error: Calling setState during render may trigger an infinite loop
24+
25+
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
26+
27+
error.invalid-unconditional-set-state-prop-in-render.ts:5:2
28+
3 | const aliased = setX;
29+
4 |
30+
> 5 | setX(1);
31+
| ^^^^ Found setState() in render
32+
6 | aliased(2);
33+
7 |
34+
8 | return x;
35+
36+
Error: Calling setState during render may trigger an infinite loop
37+
38+
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
39+
40+
error.invalid-unconditional-set-state-prop-in-render.ts:6:2
41+
4 |
42+
5 | setX(1);
43+
> 6 | aliased(2);
44+
| ^^^^^^^ Found setState() in render
45+
7 |
46+
8 | return x;
47+
9 | }
48+
```
49+
50+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters
2+
function Component({setX}) {
3+
const aliased = setX;
4+
5+
setX(1);
6+
aliased(2);
7+
8+
return x;
9+
}

0 commit comments

Comments
 (0)