Skip to content

Commit d55010c

Browse files
Copilotgsimone
andcommitted
Add StrictMode reproduction story
Add comprehensive Storybook story demonstrating the StrictMode fix with async component mounting and nested folders. Shows controls render correctly in StrictMode with delayed content layout, reproducing and validating the fix for issue #552. Co-authored-by: gsimone <[email protected]>
1 parent 2867f9f commit d55010c

File tree

1 file changed

+205
-0
lines changed

1 file changed

+205
-0
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import React, { StrictMode, useState, useEffect } from 'react'
2+
import { StoryFn, Meta } from '@storybook/react'
3+
import { expect, within, waitFor } from 'storybook/test'
4+
5+
import Reset from '../components/decorator-reset'
6+
import { useControls, folder } from '../../src'
7+
8+
export default {
9+
title: 'Advanced/StrictMode',
10+
decorators: [Reset],
11+
} as Meta
12+
13+
/**
14+
* This story reproduces the issue where controls don't render correctly
15+
* in StrictMode when used with dynamically mounted components (like R3F Canvas).
16+
* The issue was that the useToggle hook's height calculation would run
17+
* prematurely during StrictMode's double-invocation.
18+
*/
19+
20+
// Simulates a component that mounts asynchronously (like R3F Canvas content)
21+
const AsyncMountedComponent = ({ delay = 100 }: { delay?: number }) => {
22+
const [mounted, setMounted] = useState(false)
23+
24+
useEffect(() => {
25+
const timer = setTimeout(() => setMounted(true), delay)
26+
return () => clearTimeout(timer)
27+
}, [delay])
28+
29+
const values = useControls('Async Component', {
30+
position: { value: { x: 0, y: 0, z: 0 }, step: 0.1 },
31+
scale: { value: 1, min: 0.1, max: 2, step: 0.1 },
32+
color: '#ff0000',
33+
visible: true,
34+
settings: folder({
35+
wireframe: false,
36+
castShadow: true,
37+
receiveShadow: true,
38+
}),
39+
})
40+
41+
if (!mounted) return <div>Loading...</div>
42+
43+
return (
44+
<div style={{ padding: 20, background: '#f0f0f0', marginTop: 20 }}>
45+
<h3>Async Mounted Component</h3>
46+
<pre data-testid="async-output">{JSON.stringify(values, null, 2)}</pre>
47+
</div>
48+
)
49+
}
50+
51+
const NestedFoldersComponent = () => {
52+
const values = useControls('Nested Folders', {
53+
basic: 1,
54+
folder1: folder({
55+
value1: 'test',
56+
value2: 42,
57+
nested: folder({
58+
deep: true,
59+
color: '#00ff00',
60+
}),
61+
}),
62+
folder2: folder(
63+
{
64+
collapsed: 'initial',
65+
data: [1, 2, 3],
66+
},
67+
{ collapsed: true }
68+
),
69+
})
70+
71+
return (
72+
<div style={{ padding: 20, background: '#e0e0e0', marginTop: 20 }}>
73+
<h3>Nested Folders Component</h3>
74+
<pre data-testid="nested-output">{JSON.stringify(values, null, 2)}</pre>
75+
</div>
76+
)
77+
}
78+
79+
const BaseTemplate: StoryFn<{ useStrictMode: boolean; delay?: number }> = ({ useStrictMode, delay = 100 }) => {
80+
const Wrapper = useStrictMode ? StrictMode : React.Fragment
81+
82+
return (
83+
<Wrapper>
84+
<div>
85+
<div
86+
style={{
87+
padding: 10,
88+
background: useStrictMode ? '#fff3cd' : '#d1ecf1',
89+
border: `2px solid ${useStrictMode ? '#ffc107' : '#0dcaf0'}`,
90+
marginBottom: 20,
91+
}}>
92+
<strong>Mode: {useStrictMode ? 'StrictMode Enabled' : 'Normal Mode'}</strong>
93+
<p style={{ margin: '5px 0 0 0', fontSize: '14px' }}>
94+
{useStrictMode
95+
? 'React StrictMode causes effects to run twice. Controls should still render correctly.'
96+
: 'Running in normal mode without StrictMode.'}
97+
</p>
98+
</div>
99+
<AsyncMountedComponent delay={delay} />
100+
<NestedFoldersComponent />
101+
</div>
102+
</Wrapper>
103+
)
104+
}
105+
106+
export const WithStrictMode = BaseTemplate.bind({})
107+
WithStrictMode.args = {
108+
useStrictMode: true,
109+
delay: 100,
110+
}
111+
WithStrictMode.play = async ({ canvasElement }) => {
112+
const canvas = within(canvasElement)
113+
114+
// Wait for the async component to mount
115+
await waitFor(
116+
() => {
117+
expect(canvas.getByText(/Async Mounted Component/i)).toBeInTheDocument()
118+
},
119+
{ timeout: 3000 }
120+
)
121+
122+
// Verify the Leva panel is rendered and visible
123+
await waitFor(
124+
() => {
125+
const levaPanel = within(document.body).queryByText(/Async Component/i)
126+
expect(levaPanel).toBeInTheDocument()
127+
},
128+
{ timeout: 3000 }
129+
)
130+
131+
// Verify controls are interactive - find a control by its label
132+
await waitFor(
133+
() => {
134+
const scaleInput = within(document.body).queryByLabelText(/scale/i)
135+
expect(scaleInput).toBeInTheDocument()
136+
},
137+
{ timeout: 3000 }
138+
)
139+
140+
// Verify nested folders are rendered
141+
await waitFor(
142+
() => {
143+
const nestedPanel = within(document.body).queryByText(/Nested Folders/i)
144+
expect(nestedPanel).toBeInTheDocument()
145+
},
146+
{ timeout: 3000 }
147+
)
148+
}
149+
150+
export const WithoutStrictMode = BaseTemplate.bind({})
151+
WithoutStrictMode.args = {
152+
useStrictMode: false,
153+
delay: 100,
154+
}
155+
WithoutStrictMode.play = WithStrictMode.play
156+
157+
export const StrictModeWithSlowMount = BaseTemplate.bind({})
158+
StrictModeWithSlowMount.args = {
159+
useStrictMode: true,
160+
delay: 500,
161+
}
162+
StrictModeWithSlowMount.parameters = {
163+
docs: {
164+
description: {
165+
story:
166+
'Tests the fix with a slower async mount to ensure controls render correctly even with delayed content layout.',
167+
},
168+
},
169+
}
170+
171+
// Component that toggles between StrictMode and normal mode
172+
export const InteractiveModeToggle: StoryFn = () => {
173+
const [strictMode, setStrictMode] = useState(true)
174+
const Wrapper = strictMode ? StrictMode : React.Fragment
175+
176+
return (
177+
<div>
178+
<button
179+
onClick={() => setStrictMode((s) => !s)}
180+
style={{
181+
padding: '10px 20px',
182+
marginBottom: 20,
183+
fontSize: '16px',
184+
cursor: 'pointer',
185+
background: strictMode ? '#ffc107' : '#0dcaf0',
186+
border: 'none',
187+
borderRadius: '4px',
188+
color: '#000',
189+
}}>
190+
Toggle StrictMode (Currently: {strictMode ? 'ON' : 'OFF'})
191+
</button>
192+
<Wrapper key={strictMode ? 'strict' : 'normal'}>
193+
<AsyncMountedComponent delay={100} />
194+
<NestedFoldersComponent />
195+
</Wrapper>
196+
</div>
197+
)
198+
}
199+
InteractiveModeToggle.parameters = {
200+
docs: {
201+
description: {
202+
story: 'Interactive story that allows toggling between StrictMode and normal mode to verify the fix works in both cases.',
203+
},
204+
},
205+
}

0 commit comments

Comments
 (0)