@samitouri / QOS-React / commits / 7899729130

[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 } ```

Joseph Savona committed Sep 16, 2025 at 15:48 UTC 78997291302c42b788dafa4c12da246e44be2dda
6 files changed +144 -1
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+7
@@ -621,6 +621,13 @@ export const EnvironmentConfigSchema = z.object({
621 */
622 enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
623
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 +
631 /*
632 * If specified a value, the compiler lowers any calls to `useContext` to use
633 * this value as the callee.
compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts
+9 -1
@@ -31,6 +31,7 @@ import {
31 BuiltInObjectId,
32 BuiltInPropsId,
33 BuiltInRefValueId,
34 + BuiltInSetStateId,
35 BuiltInUseRefId,
36 } from '../HIR/ObjectShape';
37 import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
@@ -276,9 +277,16 @@ function* generateInstructionTypes(
277 * We should change Hook to a subtype of Function or change unifier logic.
278 * (see https://github.com/facebook/react-forget/pull/1427)
279 */
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 + }
287 yield equation(value.callee.identifier.type, {
288 kind: 'Function',
281 - shapeId: null,
289 + shapeId,
290 return: returnType,
291 isConstructor: false,
292 });
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md new
+55
@@ -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 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.js new
+14
@@ -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 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md new
+50
@@ -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 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.js new
+9
@@ -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 +}