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 fc5ba4038170e..dee573d4b8cfc 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 calling setState directly in render. + */ + 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 a1a05b2e63c0a..56fd4c14c3499 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts @@ -10,11 +10,116 @@ import { CompilerError, ErrorCategory, } from '../CompilerError'; -import {HIRFunction, IdentifierId, isSetStateType} from '../HIR'; +import { + HIRFunction, + Identifier, + IdentifierId, + Instruction, + InstructionValue, + isPrimitiveType, + isSetStateType, + Phi, + Place, + SpreadPattern, +} from '../HIR'; import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks'; import {eachInstructionValueOperand} from '../HIR/visitors'; import {Result} from '../Utils/Result'; +function isPrimitiveSetArg( + arg: Place | SpreadPattern, + fn: HIRFunction, +): boolean { + if (arg.kind !== 'Identifier') { + return false; + } + + const visited = new Set(); + const defs = buildDefinitionMap(fn); + return isPrimitiveIdentifier(arg.identifier, defs, visited); +} + +type DefinitionMap = Map; + +function buildDefinitionMap(fn: HIRFunction): DefinitionMap { + const defs: DefinitionMap = new Map(); + + for (const [, block] of fn.body.blocks) { + for (const phi of block.phis) { + defs.set(phi.place.identifier.id, phi); + } + for (const instr of block.instructions) { + defs.set(instr.lvalue.identifier.id, instr); + } + } + + return defs; +} + +function isPrimitiveIdentifier( + identifier: Identifier, + defs: DefinitionMap, + visited: Set, +): boolean { + if (isPrimitiveType(identifier)) { + return true; + } + + if (visited.has(identifier.id)) { + return false; + } + visited.add(identifier.id); + + const def = defs.get(identifier.id); + if (def == null) { + return false; + } + + if (isPhi(def)) { + return Array.from(def.operands.values()).every(operand => + isPrimitiveIdentifier(operand.identifier, defs, visited), + ); + } + + return isPrimitiveInstruction(def.value, defs, visited); +} + +function isPhi(def: Instruction | Phi): def is Phi { + return 'kind' in def && def.kind === 'Phi'; +} + +function isPrimitiveInstruction( + value: InstructionValue, + defs: DefinitionMap, + visited: Set, +): boolean { + switch (value.kind) { + case 'Primitive': + case 'TemplateLiteral': + case 'JSXText': + case 'UnaryExpression': + case 'BinaryExpression': + return true; + + case 'TypeCastExpression': + return isPrimitiveIdentifier(value.value.identifier, defs, visited); + + case 'LoadLocal': + case 'LoadContext': + return isPrimitiveIdentifier(value.place.identifier, defs, visited); + + case 'StoreLocal': + case 'StoreContext': + return isPrimitiveIdentifier(value.value.identifier, defs, visited); + + case 'Await': + return isPrimitiveIdentifier(value.value.identifier, defs, visited); + + default: + return false; + } +} + /** * Validates that the given function does not have an infinite update loop * caused by unconditionally calling setState during render. This validation @@ -55,6 +160,7 @@ function validateNoSetStateInRenderImpl( unconditionalSetStateFunctions: Set, ): Result { const unconditionalBlocks = computeUnconditionalBlocks(fn); + const enableUseKeyedState = fn.env.config.enableUseKeyedState; let activeManualMemoId: number | null = null; const errors = new CompilerError(); for (const [, block] of fn.body.blocks) { @@ -155,20 +261,48 @@ 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', - }), - ); + let isArgPrimitive = false; + + if (instr.value.args.length > 0) { + const arg = instr.value.args[0]; + if (arg.kind === 'Identifier') { + isArgPrimitive = isPrimitiveSetArg(arg, fn); + } + } + + if (isArgPrimitive) { + if (enableUseKeyedState) { + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.RenderSetState, + reason: + 'Calling setState during render may trigger an infinite loop', + description: + 'Use useKeyedState instead of calling setState directly in render. Example: const [value, setValue] = useKeyedState(initialValue, key)', + suggestions: null, + }).withDetails({ + kind: 'error', + loc: callee.loc, + message: 'Found setState() in render', + }), + ); + } + } else { + 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', + }), + ); + } } } 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 deleted file mode 100644 index 423076cc3a4b1..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.expect.md +++ /dev/null @@ -1,41 +0,0 @@ - -## Input - -```javascript -function Component(props) { - // Intentionally don't bind state, this repros a bug where we didn't - // infer the type of destructured properties after a hole in the array - let [, setState] = useState(); - setState(1); - return props.foo; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: ['TodoAdd'], - isComponent: 'TodoAdd', -}; - -``` - - -## Error - -``` -Found 1 error: - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.invalid-setState-in-render-unbound-state.ts:5:2 - 3 | // infer the type of destructured properties after a hole in the array - 4 | let [, setState] = useState(); -> 5 | setState(1); - | ^^^^^^^^ Found setState() in render - 6 | return props.foo; - 7 | } - 8 | -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-enabled-use-keyed-state.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-enabled-use-keyed-state.expect.md new file mode 100644 index 0000000000000..bb0dfb23ab67f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-enabled-use-keyed-state.expect.md @@ -0,0 +1,42 @@ + +## 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: Calling setState during render may trigger an infinite loop + +Use useKeyedState instead of calling setState directly in render. Example: const [value, setValue] = useKeyedState(initialValue, key). + +error.invalid-setstate-enabled-use-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-enabled-use-keyed-state.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-enabled-use-keyed-state.js new file mode 100644 index 0000000000000..46393b5ef821f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-enabled-use-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-setstate-object-no-keyed-state.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-object-no-keyed-state.expect.md new file mode 100644 index 0000000000000..9b993926660e6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-object-no-keyed-state.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @validateNoSetStateInRender +import {useState} from 'react'; + +function Component() { + const [total, setTotal] = useState(0); + setTotal({count: 42}); + return total; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true, +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Calling setState during render may trigger an infinite loop + +Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). + +error.invalid-setstate-object-no-keyed-state.ts:6:2 + 4 | function Component() { + 5 | const [total, setTotal] = useState(0); +> 6 | setTotal({count: 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-object-no-keyed-state.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-object-no-keyed-state.js new file mode 100644 index 0000000000000..5fc1fae6469ed --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setstate-object-no-keyed-state.js @@ -0,0 +1,14 @@ +// @validateNoSetStateInRender +import {useState} from 'react'; + +function Component() { + const [total, setTotal] = useState(0); + setTotal({count: 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 deleted file mode 100644 index fcd2f7c4569e6..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md +++ /dev/null @@ -1,55 +0,0 @@ - -## Input - -```javascript -// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters -function Component() { - const [state, setState] = useCustomState(0); - const aliased = setState; - - setState(1); - aliased(2); - - return state; -} - -function useCustomState(init) { - return useState(init); -} - -``` - - -## Error - -``` -Found 2 errors: - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.invalid-unconditional-set-state-hook-return-in-render.ts:6:2 - 4 | const aliased = setState; - 5 | -> 6 | setState(1); - | ^^^^^^^^ Found setState() in render - 7 | aliased(2); - 8 | - 9 | return state; - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.invalid-unconditional-set-state-hook-return-in-render.ts:7:2 - 5 | - 6 | setState(1); -> 7 | aliased(2); - | ^^^^^^^ Found setState() in render - 8 | - 9 | return state; - 10 | } -``` - - \ No newline at end of file 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 deleted file mode 100644 index 78deea8390458..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-in-render.expect.md +++ /dev/null @@ -1,51 +0,0 @@ - -## Input - -```javascript -// @validateNoSetStateInRender -function Component(props) { - const [x, setX] = useState(0); - const aliased = setX; - - setX(1); - aliased(2); - - return x; -} - -``` - - -## Error - -``` -Found 2 errors: - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.invalid-unconditional-set-state-in-render.ts:6:2 - 4 | const aliased = setX; - 5 | -> 6 | setX(1); - | ^^^^ Found setState() in render - 7 | aliased(2); - 8 | - 9 | return x; - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.invalid-unconditional-set-state-in-render.ts:7:2 - 5 | - 6 | setX(1); -> 7 | aliased(2); - | ^^^^^^^ Found setState() in render - 8 | - 9 | return x; - 10 | } -``` - - \ No newline at end of file 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 deleted file mode 100644 index 1a3eb1b7c6a92..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md +++ /dev/null @@ -1,50 +0,0 @@ - -## Input - -```javascript -// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters -function Component({setX}) { - const aliased = setX; - - setX(1); - aliased(2); - - return x; -} - -``` - - -## Error - -``` -Found 2 errors: - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.invalid-unconditional-set-state-prop-in-render.ts:5:2 - 3 | const aliased = setX; - 4 | -> 5 | setX(1); - | ^^^^ Found setState() in render - 6 | aliased(2); - 7 | - 8 | return x; - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.invalid-unconditional-set-state-prop-in-render.ts:6:2 - 4 | - 5 | setX(1); -> 6 | aliased(2); - | ^^^^^^^ Found setState() in render - 7 | - 8 | return x; - 9 | } -``` - - \ No newline at end of file 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 deleted file mode 100644 index 8ccb4f2dee704..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop-break.expect.md +++ /dev/null @@ -1,41 +0,0 @@ - -## Input - -```javascript -// @validateNoSetStateInRender -function Component(props) { - const [state, setState] = useState(false); - for (const _ of props) { - if (props.cond) { - break; - } else { - continue; - } - } - setState(true); - return state; -} - -``` - - -## Error - -``` -Found 1 error: - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.unconditional-set-state-in-render-after-loop-break.ts:11:2 - 9 | } - 10 | } -> 11 | setState(true); - | ^^^^^^^^ Found setState() in render - 12 | return state; - 13 | } - 14 | -``` - - \ No newline at end of file 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 deleted file mode 100644 index df805b4795fa4..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop.expect.md +++ /dev/null @@ -1,36 +0,0 @@ - -## Input - -```javascript -// @validateNoSetStateInRender -function Component(props) { - const [state, setState] = useState(false); - for (const _ of props) { - } - setState(true); - return state; -} - -``` - - -## Error - -``` -Found 1 error: - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.unconditional-set-state-in-render-after-loop.ts:6:2 - 4 | for (const _ of props) { - 5 | } -> 6 | setState(true); - | ^^^^^^^^ Found setState() in render - 7 | return state; - 8 | } - 9 | -``` - - \ No newline at end of file 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 deleted file mode 100644 index 313b2ed0e4a0f..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-with-loop-throw.expect.md +++ /dev/null @@ -1,41 +0,0 @@ - -## Input - -```javascript -// @validateNoSetStateInRender -function Component(props) { - const [state, setState] = useState(false); - for (const _ of props) { - if (props.cond) { - break; - } else { - throw new Error('bye!'); - } - } - setState(true); - return state; -} - -``` - - -## Error - -``` -Found 1 error: - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.unconditional-set-state-in-render-with-loop-throw.ts:11:2 - 9 | } - 10 | } -> 11 | setState(true); - | ^^^^^^^^ Found setState() in render - 12 | return state; - 13 | } - 14 | -``` - - \ No newline at end of file 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 deleted file mode 100644 index 1c89b5c9f21dd..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-lambda.expect.md +++ /dev/null @@ -1,39 +0,0 @@ - -## Input - -```javascript -// @validateNoSetStateInRender -function Component(props) { - const [x, setX] = useState(0); - - const foo = () => { - setX(1); - }; - foo(); - - return [x]; -} - -``` - - -## Error - -``` -Found 1 error: - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.unconditional-set-state-lambda.ts:8:2 - 6 | setX(1); - 7 | }; -> 8 | foo(); - | ^^^ Found setState() in render - 9 | - 10 | return [x]; - 11 | } -``` - - \ No newline at end of file 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 deleted file mode 100644 index fceed8b192ff4..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-nested-function-expressions.expect.md +++ /dev/null @@ -1,47 +0,0 @@ - -## Input - -```javascript -// @validateNoSetStateInRender -function Component(props) { - const [x, setX] = useState(0); - - const foo = () => { - setX(1); - }; - - const bar = () => { - foo(); - }; - - const baz = () => { - bar(); - }; - baz(); - - return [x]; -} - -``` - - -## Error - -``` -Found 1 error: - -Error: Calling setState during render may trigger an infinite loop - -Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). - -error.unconditional-set-state-nested-function-expressions.ts:16:2 - 14 | bar(); - 15 | }; -> 16 | baz(); - | ^^^ Found setState() in render - 17 | - 18 | return [x]; - 19 | } -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/setState-in-render-unbound-state.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/setState-in-render-unbound-state.expect.md new file mode 100644 index 0000000000000..5b0bca52a0035 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/setState-in-render-unbound-state.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +function Component(props) { + // Intentionally don't bind state, this repros a bug where we didn't + // infer the type of destructured properties after a hole in the array + let [, setState] = useState(); + setState(1); + return props.foo; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true, +}; + +``` + +## Code + +```javascript +function Component(props) { + const [, setState] = useState(); + setState(1); + return props.foo; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true, +}; + +``` + +### Eval output +(kind: exception) useState is not defined \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/setState-in-render-unbound-state.js similarity index 85% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/setState-in-render-unbound-state.js index 58e2837692a1d..adb2470bb19bd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/setState-in-render-unbound-state.js @@ -8,6 +8,6 @@ function Component(props) { export const FIXTURE_ENTRYPOINT = { fn: Component, - params: ['TodoAdd'], - isComponent: 'TodoAdd', + params: [], + isComponent: true, }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-hook-return-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-hook-return-in-render.expect.md new file mode 100644 index 0000000000000..d89e987a3fa72 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-hook-return-in-render.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters +function Component() { + const [state, setState] = useCustomState(0); + const aliased = setState; + + setState(1); + aliased(2); + + return state; +} + +function useCustomState(init) { + return useState(init); +} + +``` + +## Code + +```javascript +// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters +function Component() { + const [state, setState] = useCustomState(0); + const aliased = setState; + + setState(1); + aliased(2); + return state; +} + +function useCustomState(init) { + return useState(init); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-hook-return-in-render.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-hook-return-in-render.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-after-loop-break.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-after-loop-break.expect.md new file mode 100644 index 0000000000000..f73ef9246266f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-after-loop-break.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +// @validateNoSetStateInRender +function Component(props) { + const [state, setState] = useState(false); + for (const _ of props) { + if (props.cond) { + break; + } else { + continue; + } + } + setState(true); + return state; +} + +``` + +## Code + +```javascript +// @validateNoSetStateInRender +function Component(props) { + const [state, setState] = useState(false); + for (const _ of props) { + if (props.cond) { + break; + } + } + + setState(true); + return state; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop-break.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-after-loop-break.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop-break.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-after-loop-break.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-after-loop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-after-loop.expect.md new file mode 100644 index 0000000000000..5c68591912274 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-after-loop.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +// @validateNoSetStateInRender +function Component(props) { + const [state, setState] = useState(false); + for (const _ of props) { + } + setState(true); + return state; +} + +``` + +## Code + +```javascript +// @validateNoSetStateInRender +function Component(props) { + const [state, setState] = useState(false); + for (const _ of props) { + } + + setState(true); + return state; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-after-loop.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-after-loop.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-after-loop.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-with-loop-throw.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-with-loop-throw.expect.md new file mode 100644 index 0000000000000..01f0035bbb055 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-with-loop-throw.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @validateNoSetStateInRender +function Component(props) { + const [state, setState] = useState(false); + for (const _ of props) { + if (props.cond) { + break; + } else { + throw new Error('bye!'); + } + } + setState(true); + return state; +} + +``` + +## Code + +```javascript +// @validateNoSetStateInRender +function Component(props) { + const [state, setState] = useState(false); + for (const _ of props) { + if (props.cond) { + break; + } else { + throw new Error("bye!"); + } + } + + setState(true); + return state; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-with-loop-throw.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-with-loop-throw.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-in-render-with-loop-throw.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render-with-loop-throw.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render.expect.md new file mode 100644 index 0000000000000..cb4e40e58dff6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @validateNoSetStateInRender +function Component(props) { + const [x, setX] = useState(0); + const aliased = setX; + + setX(1); + aliased(2); + + return x; +} + +``` + +## Code + +```javascript +// @validateNoSetStateInRender +function Component(props) { + const [x, setX] = useState(0); + const aliased = setX; + + setX(1); + aliased(2); + return x; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-in-render.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-in-render.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-lambda.expect.md new file mode 100644 index 0000000000000..0d395d1f4ae5e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-lambda.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +// @validateNoSetStateInRender +function Component(props) { + const [x, setX] = useState(0); + + const foo = () => { + setX(1); + }; + foo(); + + return [x]; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInRender +function Component(props) { + const $ = _c(2); + const [x, setX] = useState(0); + + const foo = () => { + setX(1); + }; + + foo(); + let t0; + if ($[0] !== x) { + t0 = [x]; + $[0] = x; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-lambda.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-lambda.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-lambda.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-lambda.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-nested-function-expressions.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-nested-function-expressions.expect.md new file mode 100644 index 0000000000000..322aab8108979 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-nested-function-expressions.expect.md @@ -0,0 +1,62 @@ + +## Input + +```javascript +// @validateNoSetStateInRender +function Component(props) { + const [x, setX] = useState(0); + + const foo = () => { + setX(1); + }; + + const bar = () => { + foo(); + }; + + const baz = () => { + bar(); + }; + baz(); + + return [x]; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInRender +function Component(props) { + const $ = _c(2); + const [x, setX] = useState(0); + + const foo = () => { + setX(1); + }; + + const bar = () => { + foo(); + }; + + const baz = () => { + bar(); + }; + + baz(); + let t0; + if ($[0] !== x) { + t0 = [x]; + $[0] = x; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-nested-function-expressions.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-nested-function-expressions.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.unconditional-set-state-nested-function-expressions.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-nested-function-expressions.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-prop-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-prop-in-render.expect.md new file mode 100644 index 0000000000000..f3ca3ac327c7e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-prop-in-render.expect.md @@ -0,0 +1,33 @@ + +## Input + +```javascript +// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters +function Component({setX}) { + const aliased = setX; + + setX(1); + aliased(2); + + return x; +} + +``` + +## Code + +```javascript +// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters +function Component(t0) { + const { setX } = t0; + const aliased = setX; + + setX(1); + aliased(2); + return x; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-prop-in-render.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/unconditional-set-state-prop-in-render.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary-prop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary-prop.expect.md new file mode 100644 index 0000000000000..138dde372fb80 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary-prop.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @validateNoSetStateInRender +import {useState} from 'react'; + +function Component({data, setTotal}) { + setTotal(data.rows?.count != null ? data.rows?.count : 0); + return null; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{data: {rows: {count: 3}}, setTotal: () => {}}], + isComponent: true, +}; + +``` + +## Code + +```javascript +// @validateNoSetStateInRender +import { useState } from "react"; + +function Component(t0) { + const { data, setTotal } = t0; + setTotal(data.rows?.count != null ? data.rows?.count : 0); + return null; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ data: { rows: { count: 3 } }, setTotal: () => {} }], + isComponent: true, +}; + +``` + +### Eval output +(kind: ok) null \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary-prop.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary-prop.js new file mode 100644 index 0000000000000..763bf28065cfd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary-prop.js @@ -0,0 +1,13 @@ +// @validateNoSetStateInRender +import {useState} from 'react'; + +function Component({data, setTotal}) { + setTotal(data.rows?.count != null ? data.rows?.count : 0); + return null; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{data: {rows: {count: 3}}, setTotal: () => {}}], + isComponent: true, +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary.expect.md new file mode 100644 index 0000000000000..3549511058223 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary.expect.md @@ -0,0 +1,43 @@ + +## Input + +```javascript +// @validateNoSetStateInRender +import {useState} from 'react'; + +function Component() { + const [total, setTotal] = useState(0); + setTotal(42); + return total; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true, +}; + +``` + +## Code + +```javascript +// @validateNoSetStateInRender +import { useState } from "react"; + +function Component() { + const [total, setTotal] = useState(0); + setTotal(42); + return total; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true, +}; + +``` + +### Eval output +(kind: exception) Too many re-renders. React limits the number of renders to prevent an infinite loop. \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary.js new file mode 100644 index 0000000000000..f09e2553f0e6e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/validate-no-setstate-optional-chain-ternary.js @@ -0,0 +1,14 @@ +// @validateNoSetStateInRender +import {useState} from 'react'; + +function Component() { + const [total, setTotal] = useState(0); + setTotal(42); + return total; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], + isComponent: true, +};