diff --git a/packages/react-web/src/states/valtio.tsx b/packages/react-web/src/states/valtio.tsx index 01dda709e86..0b005727cc8 100644 --- a/packages/react-web/src/states/valtio.tsx +++ b/packages/react-web/src/states/valtio.tsx @@ -318,10 +318,13 @@ function create$StateProxy( Reflect.set(target, property, value, receiver); if (nextSpec?.onChangeProp) { const pathKey = JSON.stringify(nextPath); - const isInitOnChange = !$$state.initializedLeafPaths.has(pathKey); + const isInitOnChange = + // If we are dealing with a valueProp it means that this state cell value is provided by the parent + // and we don't need to consider initialization calls for it. + !nextSpec.valueProp && !$$state.initializedLeafPaths.has(pathKey); // We need to call the onChangeProp during initialization process so that the parent - // state can be updated with the correct value. We will provide an addtionnal parameter + // state can be updated with the correct value. We will provide an additional parameter // to the onChangeProp function to indicate that the call is made during initialization. $$state.env.$props[nextSpec.onChangeProp]?.(value, { _plasmic_state_init_: isInitOnChange, @@ -648,14 +651,10 @@ export function useDollarState( useIsomorphicLayoutEffect(() => { $$state.specTreeLeaves.forEach((node) => { const spec = node.getSpec(); - if (!spec.isRepeated && spec.type !== "private" && spec.initFunc) { - const stateCell = getStateCellFrom$StateRoot( - $state, - spec.pathObj as ObjectPath - ); - if (stateCell.initialValue === UNINITIALIZED) { - initializeStateValue($$state, stateCell, $state); - } + if (!spec.isRepeated && spec.type !== "private") { + // We only need to attempt to access the state cell to trigger initialization, + // Check create$StateProxy for more details on how this works. + getStateCellFrom$StateRoot($state, spec.pathObj as ObjectPath); } }); }, []); diff --git a/packages/react-web/src/stories/UseDollarState.stories.tsx b/packages/react-web/src/stories/UseDollarState.stories.tsx index 27d54acc8d3..fa4a9161d2d 100644 --- a/packages/react-web/src/stories/UseDollarState.stories.tsx +++ b/packages/react-web/src/stories/UseDollarState.stories.tsx @@ -2,7 +2,13 @@ import { expect } from "@storybook/jest"; import { Story } from "@storybook/react"; import { userEvent, within } from "@storybook/testing-library"; import React from "react"; -import { generateStateOnChangeProp, get, set, useDollarState } from "../states"; +import { + generateStateOnChangeProp, + generateStateValueProp, + get, + set, + useDollarState, +} from "../states"; import { CyclicStatesReferencesError } from "../states/errors"; import { $StateSpec } from "../states/types"; @@ -2265,8 +2271,7 @@ const _IsOnChangePropImmediatelyFired: Story<{}> = (props) => { ], { $props: props, - }, - { inCanvas: true } + } ); return (
@@ -2349,6 +2354,199 @@ IsOnChangePropImmediatelyFired.play = async ({ canvasElement }) => { ).toEqual(`true`); }; +const _IsOnChangeFiredWithCorrectInitialValue: Story<{}> = (props) => { + const Counter = React.useCallback( + (props: { + onCountChange: (val: number) => void; + delta: number; + onDeltaChange: (val: number) => void; + }) => { + const $state = useDollarState( + [ + { + path: "count", + type: "readonly", + onChangeProp: "onCountChange", + initVal: 5, + variableType: "number", + }, + { + path: "delta", + type: "writable", + variableType: "number", + valueProp: "delta", + onChangeProp: "onDeltaChange", + }, + ], + { + $props: props, + } + ); + return ( +
+ +
+ + {$state.delta} +
+ + +
+ ); + }, + [] + ); + + const $state = useDollarState( + [ + { + path: "counter.count", + type: "private", + variableType: "number", + }, + { + path: "counter.delta", + type: "private", + variableType: "number", + initVal: 1, + }, + { + path: "invocationsCount", + type: "writable", + variableType: "array", + initVal: [], + }, + { + path: "invocationsDelta", + type: "writable", + variableType: "array", + initVal: [], + }, + ], + { + $props: props, + } + ); + + return ( +
+ { + generateStateOnChangeProp($state, ["counter", "count"]).apply( + null, + args + ); + $state.invocationsCount.push(args); + }} + delta={generateStateValueProp($state, ["counter", "delta"])} + onDeltaChange={(...args: any) => { + generateStateOnChangeProp($state, ["counter", "delta"]).apply( + null, + args + ); + $state.invocationsDelta.push(args); + }} + /> +
+ + {$state.counter.count} + +
+ + +
+ + {$state.counter.delta} +
+ + + {JSON.stringify($state.invocationsCount)} + + + + {JSON.stringify($state.invocationsDelta)} + +
+ ); +}; + +export const IsOnChangeFiredWithCorrectInitialValue = + _IsOnChangeFiredWithCorrectInitialValue.bind({}); +IsOnChangeFiredWithCorrectInitialValue.args = {}; +IsOnChangeFiredWithCorrectInitialValue.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await sleep(1); + + expect( + (canvas.getByTestId(`counter-span`) as HTMLSpanElement).textContent + ).toEqual(`5`); + expect( + (canvas.getByTestId(`delta-span`) as HTMLSpanElement).textContent + ).toEqual(`1`); + expect( + (canvas.getByTestId(`inner-delta-span`) as HTMLSpanElement).textContent + ).toEqual(`1`); + + userEvent.click(canvas.getByTestId("twice-delta-btn")); + await sleep(1); + + expect( + (canvas.getByTestId(`delta-span`) as HTMLSpanElement).textContent + ).toEqual(`2`); + expect( + (canvas.getByTestId(`inner-delta-span`) as HTMLSpanElement).textContent + ).toEqual(`2`); + + userEvent.click(canvas.getByTestId("counter-btn")); + await sleep(1); + + expect( + (canvas.getByTestId(`counter-span`) as HTMLSpanElement).textContent + ).toEqual(`7`); + expect( + (canvas.getByTestId(`inner-delta-span`) as HTMLSpanElement).textContent + ).toEqual(`2`); + + userEvent.click(canvas.getByTestId("half-delta-btn")); + await sleep(1); + + expect( + (canvas.getByTestId(`delta-span`) as HTMLSpanElement).textContent + ).toEqual(`1`); + expect( + (canvas.getByTestId(`inner-delta-span`) as HTMLSpanElement).textContent + ).toEqual(`1`); + + expect( + (canvas.getByTestId(`invocations-count`) as HTMLSpanElement).textContent + ).toEqual( + JSON.stringify([ + [5, { _plasmic_state_init_: true }], + [7, { _plasmic_state_init_: false }], + ]) + ); + + expect( + (canvas.getByTestId(`invocations-delta`) as HTMLSpanElement).textContent + ).toEqual(JSON.stringify([[1, { _plasmic_state_init_: false }]])); // Only the changes inside the component should be recorded +}; + const _ImmutableStateCells: Story<{ people: Person[] }> = (props) => { const $state = useDollarState( [