From b2533549868b8377d74981fb2a673f53459de7b1 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Thu, 4 Dec 2025 14:25:16 -0800 Subject: [PATCH] [compiler] Add enableUseKeyedState flag and improve setState-in-render errors 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 --- .../src/HIR/Environment.ts | 6 +++ .../Validation/ValidateNoSetStateInRender.ts | 48 +++++++++++++------ ...setState-in-render-unbound-state.expect.md | 6 ++- ...e-unconditional-with-keyed-state.expect.md | 44 +++++++++++++++++ ...setstate-unconditional-with-keyed-state.js | 14 ++++++ ...-set-state-hook-return-in-render.expect.md | 12 +++-- ...nconditional-set-state-in-render.expect.md | 12 +++-- ...itional-set-state-prop-in-render.expect.md | 12 +++-- ...state-in-render-after-loop-break.expect.md | 6 ++- ...l-set-state-in-render-after-loop.expect.md | 6 ++- ...-state-in-render-with-loop-throw.expect.md | 6 ++- ...r.unconditional-set-state-lambda.expect.md | 6 ++- ...tate-nested-function-expressions.expect.md | 6 ++- 13 files changed, 146 insertions(+), 38 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-unconditional-with-keyed-state.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-unconditional-with-keyed-state.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index fc5ba403817..17dd53adf56 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -318,6 +318,12 @@ export const EnvironmentConfigSchema = z.object({ */ validateNoSetStateInRender: z.boolean().default(true), + /** + * When enabled, changes the behavior of validateNoSetStateInRender to recommend + * using useKeyedState instead of the manual pattern for resetting state. + */ + enableUseKeyedState: z.boolean().default(false), + /** * Validates that setState is not called synchronously within an effect (useEffect and friends). * Scheduling a setState (with an event listener, subscription, etc) is valid. diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts index a1a05b2e63c..e0d34d5e8e1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts @@ -155,20 +155,40 @@ function validateNoSetStateInRenderImpl( }), ); } else if (unconditionalBlocks.has(block.id)) { - errors.pushDiagnostic( - CompilerDiagnostic.create({ - category: ErrorCategory.RenderSetState, - reason: - 'Calling setState during render may trigger an infinite loop', - description: - 'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)', - suggestions: null, - }).withDetails({ - kind: 'error', - loc: callee.loc, - message: 'Found setState() in render', - }), - ); + const enableUseKeyedState = fn.env.config.enableUseKeyedState; + if (enableUseKeyedState) { + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.RenderSetState, + reason: 'Cannot call setState during render', + description: + 'Calling setState during render may trigger an infinite loop.\n' + + '* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.\n' + + '* To derive data from other state/props, compute the derived data during render without using state', + suggestions: null, + }).withDetails({ + kind: 'error', + loc: callee.loc, + message: 'Found setState() in render', + }), + ); + } else { + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.RenderSetState, + reason: 'Cannot call setState during render', + description: + 'Calling setState during render may trigger an infinite loop.\n' + + '* 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' + + '* To derive data from other state/props, compute the derived data during render without using state', + suggestions: null, + }).withDetails({ + kind: 'error', + loc: callee.loc, + message: 'Found setState() in render', + }), + ); + } } } break; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.expect.md index 423076cc3a4..43ae7d0ec2d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.expect.md @@ -24,9 +24,11 @@ export const FIXTURE_ENTRYPOINT = { ``` Found 1 error: -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.invalid-setState-in-render-unbound-state.ts:5:2 3 | // infer the type of destructured properties after a hole in the array diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-unconditional-with-keyed-state.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-unconditional-with-keyed-state.expect.md new file mode 100644 index 00000000000..7caed105de9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-unconditional-with-keyed-state.expect.md @@ -0,0 +1,44 @@ + +## Input + +```javascript +// @validateNoSetStateInRender @enableUseKeyedState +import {useState} from 'react'; + +function Component() { + const [total, setTotal] = useState(0); + setTotal(42); + return total; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true, +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot call setState during render + +Calling setState during render may trigger an infinite loop. +* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes. +* To derive data from other state/props, compute the derived data during render without using state. + +error.invalid-setstate-unconditional-with-keyed-state.ts:6:2 + 4 | function Component() { + 5 | const [total, setTotal] = useState(0); +> 6 | setTotal(42); + | ^^^^^^^^ Found setState() in render + 7 | return total; + 8 | } + 9 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-unconditional-with-keyed-state.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-unconditional-with-keyed-state.js new file mode 100644 index 00000000000..46393b5ef82 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-unconditional-with-keyed-state.js @@ -0,0 +1,14 @@ +// @validateNoSetStateInRender @enableUseKeyedState +import {useState} from 'react'; + +function Component() { + const [total, setTotal] = useState(0); + setTotal(42); + return total; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true, +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md index fcd2f7c4569..cb520546bb7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md @@ -25,9 +25,11 @@ function useCustomState(init) { ``` Found 2 errors: -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.invalid-unconditional-set-state-hook-return-in-render.ts:6:2 4 | const aliased = setState; @@ -38,9 +40,11 @@ error.invalid-unconditional-set-state-hook-return-in-render.ts:6:2 8 | 9 | return state; -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.invalid-unconditional-set-state-hook-return-in-render.ts:7:2 5 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-in-render.expect.md index 78deea83904..9155951daa5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-in-render.expect.md @@ -21,9 +21,11 @@ function Component(props) { ``` Found 2 errors: -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.invalid-unconditional-set-state-in-render.ts:6:2 4 | const aliased = setX; @@ -34,9 +36,11 @@ error.invalid-unconditional-set-state-in-render.ts:6:2 8 | 9 | return x; -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.invalid-unconditional-set-state-in-render.ts:7:2 5 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md index 1a3eb1b7c6a..8c46cbaf0f1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md @@ -20,9 +20,11 @@ function Component({setX}) { ``` Found 2 errors: -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.invalid-unconditional-set-state-prop-in-render.ts:5:2 3 | const aliased = setX; @@ -33,9 +35,11 @@ error.invalid-unconditional-set-state-prop-in-render.ts:5:2 7 | 8 | return x; -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.invalid-unconditional-set-state-prop-in-render.ts:6:2 4 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop-break.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop-break.expect.md index 8ccb4f2dee7..ad39cbc8bb7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop-break.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop-break.expect.md @@ -24,9 +24,11 @@ function Component(props) { ``` Found 1 error: -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.unconditional-set-state-in-render-after-loop-break.ts:11:2 9 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop.expect.md index df805b4795f..066c185e7af 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop.expect.md @@ -19,9 +19,11 @@ function Component(props) { ``` Found 1 error: -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.unconditional-set-state-in-render-after-loop.ts:6:2 4 | for (const _ of props) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-with-loop-throw.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-with-loop-throw.expect.md index 313b2ed0e4a..82d7cfbe286 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-with-loop-throw.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-with-loop-throw.expect.md @@ -24,9 +24,11 @@ function Component(props) { ``` Found 1 error: -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.unconditional-set-state-in-render-with-loop-throw.ts:11:2 9 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-lambda.expect.md index 1c89b5c9f21..1ebd42229d7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-lambda.expect.md @@ -22,9 +22,11 @@ function Component(props) { ``` Found 1 error: -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.unconditional-set-state-lambda.ts:8:2 6 | setX(1); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-nested-function-expressions.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-nested-function-expressions.expect.md index fceed8b192f..4736e66c124 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-nested-function-expressions.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-nested-function-expressions.expect.md @@ -30,9 +30,11 @@ function Component(props) { ``` Found 1 error: -Error: Calling setState during render may trigger an infinite loop +Error: Cannot call setState during render -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). +Calling setState during render may trigger an infinite loop. +* 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 +* To derive data from other state/props, compute the derived data during render without using state. error.unconditional-set-state-nested-function-expressions.ts:16:2 14 | bar();