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(); diff --git a/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts b/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts index 854e26149f7..b35e62677df 100644 --- a/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts +++ b/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts @@ -151,6 +151,7 @@ function makeRule(rule: LintRule): Rule.RuleModule { docs: { description: rule.description, recommended: rule.preset === LintRulePreset.Recommended, + url: `https://react.dev/reference/eslint-plugin-react-hooks/lints/${rule.name}`, }, fixable: 'code', hasSuggestions: true, diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index 39734dae7ca..742cae6d201 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -437,6 +437,11 @@ function loadServerReference, T>( if (typeof id !== 'string') { return (null: any); } + if (key === 'then') { + // This should never happen because we always serialize objects with then-functions + // as "thenable" which reduces to ReactPromise with no other fields. + return (null: any); + } const serverReference: ServerReference = resolveServerReference<$FlowFixMe>(response._bundlerConfig, id); // We expect most servers to not really need this because you'd just have all @@ -976,7 +981,17 @@ function extractIterator(response: Response, model: Array): Iterator { return model[Symbol.iterator](); } -function createModel(response: Response, model: any): any { +function createModel( + response: Response, + model: any, + parentObject: Object, + key: string, +): any { + if (key === 'then' && typeof model === 'function') { + // This should never happen because we always serialize objects with then-functions + // as "thenable" which reduces to ReactPromise with no other fields. + return null; + } return model; }