From 28a6e36218e651810e8da649e7e3fe0633584d9d Mon Sep 17 00:00:00 2001 From: Hieu Do Date: Wed, 5 Jun 2024 21:36:31 +0700 Subject: [PATCH] feat(compiler): Consider dispatch fn from useActionState and useFormState to be non-reactive --- .../src/HIR/Globals.ts | 26 ++++++++ .../src/HIR/HIR.ts | 31 +++++++++ .../src/HIR/ObjectShape.ts | 44 +++++++++++++ .../src/Inference/InferReactivePlaces.ts | 8 +-- .../PruneNonReactiveDependencies.ts | 10 +-- ...patch-considered-as-non-reactive.expect.md | 63 +++++++++++++++++++ ...ate-dispatch-considered-as-non-reactive.js | 16 +++++ ...patch-considered-as-non-reactive.expect.md | 63 +++++++++++++++++++ ...ate-dispatch-considered-as-non-reactive.js | 16 +++++ 9 files changed, 264 insertions(+), 13 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useActionState-dispatch-considered-as-non-reactive.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useActionState-dispatch-considered-as-non-reactive.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useFormState-dispatch-considered-as-non-reactive.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useFormState-dispatch-considered-as-non-reactive.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index 041d2fbf00911..6a195ca6fd00e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -9,7 +9,9 @@ import { Effect, ValueKind, ValueReason } from "./HIR"; import { BUILTIN_SHAPES, BuiltInArrayId, + BuiltInUseActionStateId, BuiltInUseEffectHookId, + BuiltInUseFormStateId, BuiltInUseInsertionEffectHookId, BuiltInUseLayoutEffectHookId, BuiltInUseOperatorId, @@ -266,6 +268,30 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ returnValueReason: ValueReason.State, }), ], + [ + "useActionState", + addHook(DEFAULT_SHAPES, { + positionalParams: [], + restParam: Effect.Freeze, + returnType: { kind: "Object", shapeId: BuiltInUseActionStateId }, + calleeEffect: Effect.Read, + hookKind: "useActionState", + returnValueKind: ValueKind.Frozen, + returnValueReason: ValueReason.State, + }), + ], + [ + "useFormState", + addHook(DEFAULT_SHAPES, { + positionalParams: [], + restParam: Effect.Freeze, + returnType: { kind: "Object", shapeId: BuiltInUseFormStateId }, + calleeEffect: Effect.Read, + hookKind: "useFormState", + returnValueKind: ValueKind.Frozen, + returnValueReason: ValueReason.State, + }), + ], [ "useReducer", addHook(DEFAULT_SHAPES, { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index afa0799b40d26..c64a9f915a7ce 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -1498,6 +1498,28 @@ export function isSetStateType(id: Identifier): boolean { return id.type.kind === "Function" && id.type.shapeId === "BuiltInSetState"; } +export function isUseActionStateType(id: Identifier): boolean { + return ( + id.type.kind === "Object" && id.type.shapeId === "BuiltInUseActionState" + ); +} + +export function isSetActionStateType(id: Identifier): boolean { + return ( + id.type.kind === "Function" && id.type.shapeId === "BuiltInSetActionState" + ); +} + +export function isUseFormStateType(id: Identifier): boolean { + return id.type.kind === "Object" && id.type.shapeId === "BuiltInUseFormState"; +} + +export function isSetFormStateType(id: Identifier): boolean { + return ( + id.type.kind === "Function" && id.type.shapeId === "BuiltInSetFormState" + ); +} + export function isUseReducerType(id: Identifier): boolean { return id.type.kind === "Function" && id.type.shapeId === "BuiltInUseReducer"; } @@ -1506,6 +1528,15 @@ export function isDispatcherType(id: Identifier): boolean { return id.type.kind === "Function" && id.type.shapeId === "BuiltInDispatch"; } +export function isStableType(id: Identifier): boolean { + return ( + isSetStateType(id) || + isSetActionStateType(id) || + isSetFormStateType(id) || + isDispatcherType(id) + ); +} + export function isUseEffectHookType(id: Identifier): boolean { return ( id.type.kind === "Function" && id.type.shapeId === "BuiltInUseEffectHook" diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index 8997ad086f5a2..653588babb92a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -118,6 +118,8 @@ function addShape( export type HookKind = | "useContext" | "useState" + | "useActionState" + | "useFormState" | "useReducer" | "useRef" | "useEffect" @@ -194,6 +196,10 @@ export const BuiltInJsxId = "BuiltInJsx"; export const BuiltInObjectId = "BuiltInObject"; export const BuiltInUseStateId = "BuiltInUseState"; export const BuiltInSetStateId = "BuiltInSetState"; +export const BuiltInUseActionStateId = "BuiltInUseActionState"; +export const BuiltInSetActionStateId = "BuiltInSetActionState"; +export const BuiltInUseFormStateId = "BuiltInUseFormState"; +export const BuiltInSetFormStateId = "BuiltInSetFormState"; export const BuiltInUseRefId = "BuiltInUseRefId"; export const BuiltInRefValueId = "BuiltInRefValue"; export const BuiltInMixedReadonlyId = "BuiltInMixedReadonly"; @@ -390,6 +396,44 @@ addObject(BUILTIN_SHAPES, BuiltInUseStateId, [ ], ]); +addObject(BUILTIN_SHAPES, BuiltInUseActionStateId, [ + ["0", { kind: "Poly" }], + [ + "1", + addFunction( + BUILTIN_SHAPES, + [], + { + positionalParams: [], + restParam: Effect.Freeze, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }, + BuiltInSetActionStateId + ), + ], +]); + +addObject(BUILTIN_SHAPES, BuiltInUseFormStateId, [ + ["0", { kind: "Poly" }], + [ + "1", + addFunction( + BUILTIN_SHAPES, + [], + { + positionalParams: [], + restParam: Effect.Freeze, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }, + BuiltInSetFormStateId + ), + ], +]); + addObject(BUILTIN_SHAPES, BuiltInUseReducerId, [ ["0", { kind: "Poly" }], [ diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts index e6a7bb49ce132..6d53c8f157400 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts @@ -15,8 +15,7 @@ import { Place, computePostDominatorTree, getHookKind, - isDispatcherType, - isSetStateType, + isStableType, isUseOperator, } from "../HIR"; import { PostDominator } from "../HIR/Dominator"; @@ -220,10 +219,7 @@ export function inferReactivePlaces(fn: HIRFunction): void { if (hasReactiveInput) { for (const lvalue of eachInstructionLValue(instruction)) { - if ( - isSetStateType(lvalue.identifier) || - isDispatcherType(lvalue.identifier) - ) { + if (isStableType(lvalue.identifier)) { continue; } reactiveIdentifiers.markReactive(lvalue); diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonReactiveDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonReactiveDependencies.ts index aef5d50ee3a06..2e1748fc6a425 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonReactiveDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonReactiveDependencies.ts @@ -10,8 +10,7 @@ import { ReactiveFunction, ReactiveInstruction, ReactiveScopeBlock, - isDispatcherType, - isSetStateType, + isStableType, } from "../HIR"; import { eachPatternOperand } from "../HIR/visitors"; import { collectReactiveIdentifiers } from "./CollectReactiveIdentifiers"; @@ -57,10 +56,7 @@ class Visitor extends ReactiveFunctionVisitor { case "Destructure": { if (state.has(value.value.identifier.id)) { for (const lvalue of eachPatternOperand(value.lvalue.pattern)) { - if ( - isSetStateType(lvalue.identifier) || - isDispatcherType(lvalue.identifier) - ) { + if (isStableType(lvalue.identifier)) { continue; } state.add(lvalue.identifier.id); @@ -75,7 +71,7 @@ class Visitor extends ReactiveFunctionVisitor { if ( lvalue !== null && state.has(value.object.identifier.id) && - !isSetStateType(lvalue.identifier) + !isStableType(lvalue.identifier) ) { state.add(lvalue.identifier.id); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useActionState-dispatch-considered-as-non-reactive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useActionState-dispatch-considered-as-non-reactive.expect.md new file mode 100644 index 0000000000000..c0366e605d7ea --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useActionState-dispatch-considered-as-non-reactive.expect.md @@ -0,0 +1,63 @@ + +## Input + +```javascript +import { useActionState } from "react"; + +function Component() { + const [actionState, dispatchAction] = useActionState(); + const onSubmitAction = () => { + dispatchAction(); + }; + return ; +} + +function Foo() {} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useActionState } from "react"; + +function Component() { + const $ = _c(2); + const [actionState, dispatchAction] = useActionState(); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + dispatchAction(); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + const onSubmitAction = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = ; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +function Foo() {} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +### Eval output +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useActionState-dispatch-considered-as-non-reactive.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useActionState-dispatch-considered-as-non-reactive.js new file mode 100644 index 0000000000000..04bf7eaeb2e51 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useActionState-dispatch-considered-as-non-reactive.js @@ -0,0 +1,16 @@ +import { useActionState } from "react"; + +function Component() { + const [actionState, dispatchAction] = useActionState(); + const onSubmitAction = () => { + dispatchAction(); + }; + return ; +} + +function Foo() {} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useFormState-dispatch-considered-as-non-reactive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useFormState-dispatch-considered-as-non-reactive.expect.md new file mode 100644 index 0000000000000..bd6ad05eeb568 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useFormState-dispatch-considered-as-non-reactive.expect.md @@ -0,0 +1,63 @@ + +## Input + +```javascript +import { useFormState } from "react-dom"; + +function Component() { + const [formState, dispatchForm] = useFormState(); + const onSubmitForm = () => { + dispatchForm(); + }; + return ; +} + +function Foo() {} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useFormState } from "react-dom"; + +function Component() { + const $ = _c(2); + const [formState, dispatchForm] = useFormState(); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + dispatchForm(); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + const onSubmitForm = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = ; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +function Foo() {} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +### Eval output +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useFormState-dispatch-considered-as-non-reactive.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useFormState-dispatch-considered-as-non-reactive.js new file mode 100644 index 0000000000000..f58903e7a54a3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useFormState-dispatch-considered-as-non-reactive.js @@ -0,0 +1,16 @@ +import { useFormState } from "react-dom"; + +function Component() { + const [formState, dispatchForm] = useFormState(); + const onSubmitForm = () => { + dispatchForm(); + }; + return ; +} + +function Foo() {} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +};