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 fc5ba4038170..17dd53adf56e 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 a1a05b2e63c0..e0d34d5e8e10 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 423076cc3a4b..43ae7d0ec2db 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 000000000000..7caed105de98 --- /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 000000000000..46393b5ef821 --- /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 fcd2f7c4569e..cb520546bb7b 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 78deea839045..9155951daa50 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 1a3eb1b7c6a9..8c46cbaf0f16 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 8ccb4f2dee70..ad39cbc8bb78 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 df805b4795fa..066c185e7af4 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 313b2ed0e4a0..82d7cfbe286c 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 1c89b5c9f21d..1ebd42229d71 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 fceed8b192ff..4736e66c1247 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();