Skip to content

Commit f99241b

Browse files
authored
[compiler] Add enableUseKeyedState flag and improve setState-in-render errors (#35230)
Adds a new `enableUseKeyedState` compiler flag that changes the error message for unconditional setState calls during render. When `enableUseKeyedState` is enabled, the error recommends using `useKeyedState(initialState, key)` to reset state when dependencies change. When disabled (the default), it links to the React docs for the manual pattern of storing previous values in state. Both error messages now include helpful bullet points explaining the two main alternatives: 1. Use useKeyedState (or manual pattern) to reset state when other state/props change 2. Compute derived data directly during render without using state
1 parent 66ae640 commit f99241b

13 files changed

+146
-38
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,12 @@ export const EnvironmentConfigSchema = z.object({
318318
*/
319319
validateNoSetStateInRender: z.boolean().default(true),
320320

321+
/**
322+
* When enabled, changes the behavior of validateNoSetStateInRender to recommend
323+
* using useKeyedState instead of the manual pattern for resetting state.
324+
*/
325+
enableUseKeyedState: z.boolean().default(false),
326+
321327
/**
322328
* Validates that setState is not called synchronously within an effect (useEffect and friends).
323329
* Scheduling a setState (with an event listener, subscription, etc) is valid.

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -155,20 +155,40 @@ function validateNoSetStateInRenderImpl(
155155
}),
156156
);
157157
} else if (unconditionalBlocks.has(block.id)) {
158-
errors.pushDiagnostic(
159-
CompilerDiagnostic.create({
160-
category: ErrorCategory.RenderSetState,
161-
reason:
162-
'Calling setState during render may trigger an infinite loop',
163-
description:
164-
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
165-
suggestions: null,
166-
}).withDetails({
167-
kind: 'error',
168-
loc: callee.loc,
169-
message: 'Found setState() in render',
170-
}),
171-
);
158+
const enableUseKeyedState = fn.env.config.enableUseKeyedState;
159+
if (enableUseKeyedState) {
160+
errors.pushDiagnostic(
161+
CompilerDiagnostic.create({
162+
category: ErrorCategory.RenderSetState,
163+
reason: 'Cannot call setState during render',
164+
description:
165+
'Calling setState during render may trigger an infinite loop.\n' +
166+
'* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.\n' +
167+
'* To derive data from other state/props, compute the derived data during render without using state',
168+
suggestions: null,
169+
}).withDetails({
170+
kind: 'error',
171+
loc: callee.loc,
172+
message: 'Found setState() in render',
173+
}),
174+
);
175+
} else {
176+
errors.pushDiagnostic(
177+
CompilerDiagnostic.create({
178+
category: ErrorCategory.RenderSetState,
179+
reason: 'Cannot call setState during render',
180+
description:
181+
'Calling setState during render may trigger an infinite loop.\n' +
182+
'* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders\n' +
183+
'* To derive data from other state/props, compute the derived data during render without using state',
184+
suggestions: null,
185+
}).withDetails({
186+
kind: 'error',
187+
loc: callee.loc,
188+
message: 'Found setState() in render',
189+
}),
190+
);
191+
}
172192
}
173193
}
174194
break;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.expect.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ export const FIXTURE_ENTRYPOINT = {
2424
```
2525
Found 1 error:
2626
27-
Error: Calling setState during render may trigger an infinite loop
27+
Error: Cannot call setState during render
2828
29-
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
29+
Calling setState during render may trigger an infinite loop.
30+
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
31+
* To derive data from other state/props, compute the derived data during render without using state.
3032
3133
error.invalid-setState-in-render-unbound-state.ts:5:2
3234
3 | // infer the type of destructured properties after a hole in the array
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateNoSetStateInRender @enableUseKeyedState
6+
import {useState} from 'react';
7+
8+
function Component() {
9+
const [total, setTotal] = useState(0);
10+
setTotal(42);
11+
return total;
12+
}
13+
14+
export const FIXTURE_ENTRYPOINT = {
15+
fn: Component,
16+
params: [],
17+
isComponent: true,
18+
};
19+
20+
```
21+
22+
23+
## Error
24+
25+
```
26+
Found 1 error:
27+
28+
Error: Cannot call setState during render
29+
30+
Calling setState during render may trigger an infinite loop.
31+
* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.
32+
* To derive data from other state/props, compute the derived data during render without using state.
33+
34+
error.invalid-setstate-unconditional-with-keyed-state.ts:6:2
35+
4 | function Component() {
36+
5 | const [total, setTotal] = useState(0);
37+
> 6 | setTotal(42);
38+
| ^^^^^^^^ Found setState() in render
39+
7 | return total;
40+
8 | }
41+
9 |
42+
```
43+
44+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @validateNoSetStateInRender @enableUseKeyedState
2+
import {useState} from 'react';
3+
4+
function Component() {
5+
const [total, setTotal] = useState(0);
6+
setTotal(42);
7+
return total;
8+
}
9+
10+
export const FIXTURE_ENTRYPOINT = {
11+
fn: Component,
12+
params: [],
13+
isComponent: true,
14+
};

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ function useCustomState(init) {
2525
```
2626
Found 2 errors:
2727
28-
Error: Calling setState during render may trigger an infinite loop
28+
Error: Cannot call setState during render
2929
30-
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
30+
Calling setState during render may trigger an infinite loop.
31+
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
32+
* To derive data from other state/props, compute the derived data during render without using state.
3133
3234
error.invalid-unconditional-set-state-hook-return-in-render.ts:6:2
3335
4 | const aliased = setState;
@@ -38,9 +40,11 @@ error.invalid-unconditional-set-state-hook-return-in-render.ts:6:2
3840
8 |
3941
9 | return state;
4042
41-
Error: Calling setState during render may trigger an infinite loop
43+
Error: Cannot call setState during render
4244
43-
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
45+
Calling setState during render may trigger an infinite loop.
46+
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
47+
* To derive data from other state/props, compute the derived data during render without using state.
4448
4549
error.invalid-unconditional-set-state-hook-return-in-render.ts:7:2
4650
5 |

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-in-render.expect.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ function Component(props) {
2121
```
2222
Found 2 errors:
2323
24-
Error: Calling setState during render may trigger an infinite loop
24+
Error: Cannot call setState during render
2525
26-
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
26+
Calling setState during render may trigger an infinite loop.
27+
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
28+
* To derive data from other state/props, compute the derived data during render without using state.
2729
2830
error.invalid-unconditional-set-state-in-render.ts:6:2
2931
4 | const aliased = setX;
@@ -34,9 +36,11 @@ error.invalid-unconditional-set-state-in-render.ts:6:2
3436
8 |
3537
9 | return x;
3638
37-
Error: Calling setState during render may trigger an infinite loop
39+
Error: Cannot call setState during render
3840
39-
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
41+
Calling setState during render may trigger an infinite loop.
42+
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
43+
* To derive data from other state/props, compute the derived data during render without using state.
4044
4145
error.invalid-unconditional-set-state-in-render.ts:7:2
4246
5 |

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ function Component({setX}) {
2020
```
2121
Found 2 errors:
2222
23-
Error: Calling setState during render may trigger an infinite loop
23+
Error: Cannot call setState during render
2424
25-
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
25+
Calling setState during render may trigger an infinite loop.
26+
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
27+
* To derive data from other state/props, compute the derived data during render without using state.
2628
2729
error.invalid-unconditional-set-state-prop-in-render.ts:5:2
2830
3 | const aliased = setX;
@@ -33,9 +35,11 @@ error.invalid-unconditional-set-state-prop-in-render.ts:5:2
3335
7 |
3436
8 | return x;
3537
36-
Error: Calling setState during render may trigger an infinite loop
38+
Error: Cannot call setState during render
3739
38-
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
40+
Calling setState during render may trigger an infinite loop.
41+
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
42+
* To derive data from other state/props, compute the derived data during render without using state.
3943
4044
error.invalid-unconditional-set-state-prop-in-render.ts:6:2
4145
4 |

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop-break.expect.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ function Component(props) {
2424
```
2525
Found 1 error:
2626
27-
Error: Calling setState during render may trigger an infinite loop
27+
Error: Cannot call setState during render
2828
29-
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
29+
Calling setState during render may trigger an infinite loop.
30+
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
31+
* To derive data from other state/props, compute the derived data during render without using state.
3032
3133
error.unconditional-set-state-in-render-after-loop-break.ts:11:2
3234
9 | }

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop.expect.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ function Component(props) {
1919
```
2020
Found 1 error:
2121
22-
Error: Calling setState during render may trigger an infinite loop
22+
Error: Cannot call setState during render
2323
24-
Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState).
24+
Calling setState during render may trigger an infinite loop.
25+
* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
26+
* To derive data from other state/props, compute the derived data during render without using state.
2527
2628
error.unconditional-set-state-in-render-after-loop.ts:6:2
2729
4 | for (const _ of props) {

0 commit comments

Comments
 (0)