Skip to content

Commit 0f9b42b

Browse files
author
Jiří Fencl
committed
fix: 🐛 clear state before setState to prevent stale keys from persisting
- Clear widget.state before calling setState when both setProps and setState are available - This prevents load()-generated state keys from merging with Storybook-provided state - Ensures Storybook args.widget.state remains authoritative during widget reuse - Add test coverage for state clearing behavior - Fix documentation example by adding missing WidgetDescription function definition
1 parent 94626c9 commit 0f9b42b

3 files changed

Lines changed: 38 additions & 0 deletions

File tree

packages/tool-storybook/src/__tests__/indexSpec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,32 @@ describe('Merkur tool storybook', () => {
168168
expect(first.state).toEqual({ count: 99 });
169169
});
170170

171+
it('should clear state before calling setState to avoid merging with load()-generated keys (hasSetProps && hasSetState)', async () => {
172+
let loader = createWidgetLoader({ widgetProperties, render });
173+
174+
let { widget: first } = await loader(storyArgs);
175+
176+
// Simulate setProps/load() adding keys to state
177+
jest.spyOn(first, 'setProps').mockImplementation(async () => {
178+
first.state = { loadGenerated: 'stale', anotherKey: 'old' };
179+
});
180+
181+
// Track what state was present when setState was called
182+
let stateBeforeSetState;
183+
jest.spyOn(first, 'setState').mockImplementation(async (newState) => {
184+
stateBeforeSetState = { ...first.state };
185+
first.state = { ...first.state, ...newState };
186+
});
187+
188+
storyArgs.args.widget.state = { count: 42 };
189+
await loader(storyArgs);
190+
191+
// State should have been cleared ({}) before setState was called
192+
expect(stateBeforeSetState).toEqual({});
193+
// Final state should only contain what setState received, not load()-generated keys
194+
expect(first.state).toEqual({ count: 42 });
195+
});
196+
171197
it('should preserve story state when only setProps is available and load() overwrites state (hasSetProps && !hasSetState)', async () => {
172198
let loader = createWidgetLoader({ widgetProperties, render });
173199

packages/tool-storybook/src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ function createWidgetLoader({ render, widgetProperties }) {
5959
if (hasSetProps && hasSetState) {
6060
widget.props = {};
6161
await widget.setProps(nextProps);
62+
// Clear state so that Storybook-provided state replaces any previous or
63+
// load()-generated keys instead of merging with them.
64+
widget.state = {};
6265
await widget.setState(nextState);
6366
return { widget };
6467
}

website/docs/storybook-integration-into-merkur.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,15 @@ The `createVanillaRenderer` in `preview.mjs` handles all the DOM creation and re
328328
For stories that need custom logic (e.g., overriding specific widget properties), you can provide a custom render function:
329329
330330
```javascript
331+
// Simple example render function for a vanilla widget.
332+
// In your project, replace this with your actual widget render implementation.
333+
const WidgetDescription = (widget) => `
334+
<div>
335+
<h2>${widget.name}</h2>
336+
<p>Version: ${widget.version}</p>
337+
</div>
338+
`;
339+
331340
export const CustomWidget = {
332341
render: (args, { loaded: { widget } }) => {
333342
const container = document.createElement('div');

0 commit comments

Comments
 (0)