Skip to content

Commit

Permalink
fix(states): Handle writable parent states during initializion of val…
Browse files Browse the repository at this point in the history
…ues and properly start firing imediate state inits

Issue: https://linear.app/plasmic/issue/PLA-11395
GitOrigin-RevId: 7e57f491044e83f523f0bf00275442d8cdb571fe
  • Loading branch information
FMota0 authored and actions-user committed Dec 26, 2024
1 parent 365d4b7 commit 60be8ff
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 13 deletions.
19 changes: 9 additions & 10 deletions packages/react-web/src/states/valtio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
});
}, []);
Expand Down
204 changes: 201 additions & 3 deletions packages/react-web/src/stories/UseDollarState.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -2265,8 +2271,7 @@ const _IsOnChangePropImmediatelyFired: Story<{}> = (props) => {
],
{
$props: props,
},
{ inCanvas: true }
}
);
return (
<div>
Expand Down Expand Up @@ -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 (
<div>
<button
onClick={() => ($state.count = $state.count + $state.delta)}
data-testid={"counter-btn"}
>
Counter Increment
</button>
<br />

<span data-testid={"inner-delta-span"}>{$state.delta}</span>
<br />

<button
onClick={() => ($state.delta = Math.max($state.delta / 2, 1))}
data-testid={"half-delta-btn"}
>
Half delta
</button>
</div>
);
},
[]
);

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 (
<div>
<Counter
onCountChange={(...args: any) => {
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);
}}
/>
<br />

<span data-testid="counter-span">{$state.counter.count}</span>

<br />

<button
onClick={() => ($state.counter.delta = $state.counter.delta * 2)}
data-testid={"twice-delta-btn"}
>
Twice delta
</button>
<br />

<span data-testid="delta-span">{$state.counter.delta}</span>
<br />

<span data-testid="invocations-count">
{JSON.stringify($state.invocationsCount)}
</span>

<span data-testid="invocations-delta">
{JSON.stringify($state.invocationsDelta)}
</span>
</div>
);
};

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(
[
Expand Down

0 comments on commit 60be8ff

Please sign in to comment.