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 0ef41866a51c2..57567f325fd9e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -621,6 +621,13 @@ export const EnvironmentConfigSchema = z.object({ */ enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true), + /** + * Treat identifiers as SetState type if both + * - they are named with a "set-" prefix + * - they are called somewhere + */ + enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false), + /* * If specified a value, the compiler lowers any calls to `useContext` to use * this value as the callee. diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 682195f8df624..1c4443e5a49a0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -31,6 +31,7 @@ import { BuiltInObjectId, BuiltInPropsId, BuiltInRefValueId, + BuiltInSetStateId, BuiltInUseRefId, } from '../HIR/ObjectShape'; import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors'; @@ -276,9 +277,16 @@ function* generateInstructionTypes( * We should change Hook to a subtype of Function or change unifier logic. * (see https://github.com/facebook/react-forget/pull/1427) */ + let shapeId: string | null = null; + if (env.config.enableTreatSetIdentifiersAsStateSetters) { + const name = getName(names, value.callee.identifier.id); + if (name.startsWith('set')) { + shapeId = BuiltInSetStateId; + } + } yield equation(value.callee.identifier.type, { kind: 'Function', - shapeId: null, + shapeId, return: returnType, isConstructor: false, }); 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 new file mode 100644 index 0000000000000..fcd2f7c4569e6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md @@ -0,0 +1,55 @@ + +## 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-hook-return-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.js new file mode 100644 index 0000000000000..ed9158ff09681 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.js @@ -0,0 +1,14 @@ +// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters +function Component() { + const [state, setState] = useCustomState(0); + const aliased = setState; + + setState(1); + aliased(2); + + return state; +} + +function useCustomState(init) { + return useState(init); +} 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 new file mode 100644 index 0000000000000..1a3eb1b7c6a92 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md @@ -0,0 +1,50 @@ + +## 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.invalid-unconditional-set-state-prop-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.js new file mode 100644 index 0000000000000..1a14c33fe3807 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.js @@ -0,0 +1,9 @@ +// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters +function Component({setX}) { + const aliased = setX; + + setX(1); + aliased(2); + + return x; +}