Skip to content

Commit dcb6ff4

Browse files
author
JiΕ™Γ­ Fencl
committed
fix: πŸ› avoid duplicate renders when reusing widget in loader
When both setState and setProps were available on a reused widget, the previous logic triggered two sequential updates β€” one per setter β€” and setProps would invoke load(), potentially overwriting story-provided state. Refactored createWidgetLoader to handle three distinct cases: - Only setState: assign props directly, use setState as single update trigger - Only setProps: assign state directly, use setProps as single update/load trigger - Both or neither: assign state/props directly, perform one lifecycle update Updated the corresponding test to assert the new single-render behaviour instead of spying on setState.
1 parent 7e90ae7 commit dcb6ff4

2 files changed

Lines changed: 21 additions & 20 deletions

File tree

β€Žpackages/tool-storybook/src/__tests__/indexSpec.jsβ€Ž

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,16 @@ describe('Merkur tool storybook', () => {
103103
expect(widget.props).toEqual({ title: 'Updated' });
104104
});
105105

106-
it('should call setState with new state when reusing widget for the same story', async () => {
106+
it('should update state directly and trigger a single render when reusing widget for the same story', async () => {
107107
let loader = createWidgetLoader({ widgetProperties, render });
108108

109-
let { widget: firstWidget } = await loader(storyArgs);
110-
jest.spyOn(firstWidget, 'setState');
109+
render.mockClear();
111110

112111
storyArgs.args.widget.state = { count: 42 };
113-
await loader(storyArgs);
112+
let { widget } = await loader(storyArgs);
114113

115-
expect(firstWidget.setState).toHaveBeenCalledWith({ count: 42 });
114+
expect(widget.state).toEqual({ count: 42 });
115+
expect(render).toHaveBeenCalledTimes(1);
116116
});
117117

118118
it('should throw when widget is created without component plugin', async () => {

β€Žpackages/tool-storybook/src/index.jsβ€Ž

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,28 @@ function createWidgetLoader({ render, widgetProperties }) {
3333
const lifeCycle = widget?.$in?.component?.lifeCycle;
3434
const hasSetState = typeof widget.setState === 'function';
3535
const hasSetProps = typeof widget.setProps === 'function';
36-
// Reset existing state/props to avoid stale keys when reusing the widget.
37-
if (hasSetState) {
36+
if (hasSetState && !hasSetProps) {
37+
// Use setState as the single source of truth for triggering an update.
3838
widget.state = {};
39+
widget.props = nextProps;
3940
await widget.setState(nextState);
40-
} else {
41-
widget.state = nextState;
41+
return { widget };
4242
}
43-
if (hasSetProps) {
43+
if (hasSetProps && !hasSetState) {
44+
// Use setProps as the single source of truth for triggering an update/load.
45+
widget.state = nextState;
4446
widget.props = {};
4547
await widget.setProps(nextProps);
46-
} else {
47-
widget.props = nextProps;
48+
return { widget };
4849
}
49-
// When setState/setProps are available, they should trigger widget.update()
50-
// via @merkur/plugin-component, so avoid forcing an extra lifecycle update.
51-
if (!hasSetState && !hasSetProps) {
52-
if (lifeCycle && typeof lifeCycle.update === 'function') {
53-
await lifeCycle.update(widget);
54-
} else {
55-
render(widget);
56-
}
50+
// When both setters are available (or neither), replace state/props directly
51+
// and perform a single lifecycle update to avoid duplicate work.
52+
widget.state = nextState;
53+
widget.props = nextProps;
54+
if (lifeCycle && typeof lifeCycle.update === 'function') {
55+
await lifeCycle.update(widget);
56+
} else {
57+
render(widget);
5758
}
5859
return { widget };
5960
}

0 commit comments

Comments
Β (0)