Skip to content

Commit bf8277d

Browse files
committed
fix: improve Storybook story code display and fix demo issues
- Add custom source code (parameters.docs.source.code) to all stories so the code panel shows meaningful example code instead of internal render wrappers - Fix controlEqual + memo demo: use explicit value in setNum to avoid stale closure, add parent render count and "Re-render Parent" button to clearly demonstrate memo behavior - Fix Double transform description: mapSetter(v => v * 2) doubles the written value, not simply "adds 2"
1 parent 603f276 commit bf8277d

2 files changed

Lines changed: 213 additions & 17 deletions

File tree

stories/basic.stories.tsx

Lines changed: 138 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,34 +56,39 @@ const MemoCounter = React.memo(function MemoCounter({count}: {count?: Control<nu
5656

5757
return (
5858
<div style={{padding: 8, border: '1px dashed #999', borderRadius: 4}}>
59-
<div style={{fontSize: 12, color: '#999', marginBottom: 4}}>Child (memo)</div>
60-
<div style={{fontSize: 12, color: '#888'}}>renders: {renderCount.current}</div>
59+
<div style={{fontSize: 12, color: '#999', marginBottom: 4}}>Child (memo) — renders: {renderCount.current}</div>
6160
<span>{num} </span>
62-
<button onClick={() => setNum((n) => n + 1)}>+1</button>
61+
<button onClick={() => setNum(num + 1)}>+1</button>
6362
</div>
6463
);
6564
}, controlEqual);
6665

6766
function ControlEqualDemo() {
68-
const [, , countA] = useControl<number>(0);
69-
const [, , countB] = useControl<number>(0);
70-
const [label, setLabel] = React.useState('Hello');
67+
const renderCount = React.useRef(0);
68+
renderCount.current++;
69+
const [countA, , controlA] = useControl<number>(0);
70+
const [countB, , controlB] = useControl<number>(0);
71+
const [tick, setTick] = React.useState(0);
7172

7273
return (
7374
<div style={{padding: 12, border: '2px solid #4a90d9', borderRadius: 8}}>
74-
<div style={{fontSize: 12, color: '#4a90d9', marginBottom: 8}}>Parent</div>
75+
<div style={{fontSize: 12, color: '#4a90d9', marginBottom: 8}}>Parent — renders: {renderCount.current}</div>
7576
<div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
7677
<p style={{margin: 0, fontSize: 14, color: '#666'}}>
77-
Changing the label re-renders the parent but not the memoized counters,
78-
because <code>controlEqual</code> compares control state values instead of references.
78+
Clicking "Re-render Parent" forces the parent to re-render (tick: {tick}),
79+
but the memoized children skip re-rendering because <code>controlEqual</code> sees
80+
their control values haven't changed.
7981
</p>
80-
<div>
81-
<label>Label: </label>
82-
<input value={label} onChange={(e) => setLabel(e.target.value)} />
83-
</div>
82+
<button onClick={() => setTick((t) => t + 1)}>Re-render Parent</button>
8483
<div style={{display: 'flex', gap: 16}}>
85-
<MemoCounter count={countA} />
86-
<MemoCounter count={countB} />
84+
<div>
85+
<div style={{fontSize: 12, color: '#888', marginBottom: 4}}>countA: {countA}</div>
86+
<MemoCounter count={controlA} />
87+
</div>
88+
<div>
89+
<div style={{fontSize: 12, color: '#888', marginBottom: 4}}>countB: {countB}</div>
90+
<MemoCounter count={controlB} />
91+
</div>
8792
</div>
8893
</div>
8994
</div>
@@ -165,6 +170,21 @@ export const Uncontrolled: Story = {
165170
description: {
166171
story: 'When no control prop is passed, the component manages its own internal state.',
167172
},
173+
source: {
174+
code: `function Counter({ count }) {
175+
const [num, setNum] = useControl(count, 0);
176+
return (
177+
<div>
178+
<span>{num}</span>
179+
<button onClick={() => setNum(n => n + 1)}>+1</button>
180+
</div>
181+
);
182+
}
183+
184+
// No control passed — Counter manages its own state
185+
<Counter />`,
186+
language: 'tsx',
187+
},
168188
},
169189
},
170190
};
@@ -176,6 +196,19 @@ export const Controlled: Story = {
176196
description: {
177197
story: 'A parent creates a control and passes it down. Both parent and child share the same state.',
178198
},
199+
source: {
200+
code: `function Parent() {
201+
const [count, setCount, control] = useControl(0);
202+
return (
203+
<div>
204+
<div>Parent sees count: {count}</div>
205+
<Counter count={control} />
206+
<button onClick={() => setCount(0)}>Reset from Parent</button>
207+
</div>
208+
);
209+
}`,
210+
language: 'tsx',
211+
},
179212
},
180213
},
181214
};
@@ -187,6 +220,20 @@ export const Siblings: Story = {
187220
description: {
188221
story: 'Multiple children share the same control — clicking +1 in either counter updates both.',
189222
},
223+
source: {
224+
code: `function SiblingCounters() {
225+
const [count, setCount, control] = useControl(0);
226+
return (
227+
<div>
228+
<div>Shared count: {count}</div>
229+
<Counter count={control} />
230+
<Counter count={control} />
231+
<button onClick={() => setCount(0)}>Reset All</button>
232+
</div>
233+
);
234+
}`,
235+
language: 'tsx',
236+
},
190237
},
191238
},
192239
};
@@ -199,6 +246,39 @@ export const WithControlEqual: Story = {
199246
description: {
200247
story: '`controlEqual` lets `React.memo` compare control props by their state values, avoiding unnecessary re-renders when the control reference changes but the value stays the same.',
201248
},
249+
source: {
250+
code: `const MemoCounter = React.memo(function MemoCounter({ count }) {
251+
const renderCount = React.useRef(0);
252+
renderCount.current++;
253+
const [num, setNum] = useControl(count, 0);
254+
255+
return (
256+
<div>
257+
<div>renders: {renderCount.current}</div>
258+
<span>{num}</span>
259+
<button onClick={() => setNum(num + 1)}>+1</button>
260+
</div>
261+
);
262+
}, controlEqual);
263+
264+
function Parent() {
265+
const renderCount = React.useRef(0);
266+
renderCount.current++;
267+
const [countA, , controlA] = useControl(0);
268+
const [countB, , controlB] = useControl(0);
269+
const [tick, setTick] = React.useState(0);
270+
271+
return (
272+
<div>
273+
<div>Parent renders: {renderCount.current}</div>
274+
<button onClick={() => setTick(t => t + 1)}>Re-render Parent</button>
275+
<MemoCounter count={controlA} />
276+
<MemoCounter count={controlB} />
277+
</div>
278+
);
279+
}`,
280+
language: 'tsx',
281+
},
202282
},
203283
},
204284
};
@@ -211,6 +291,49 @@ export const CrossLayer: Story = {
211291
description: {
212292
story: 'A single `control` flows through multiple layers of wrapper components (App → Panel → Card → Input). Every component at any depth can read and write the same state — no Context provider, no callback prop drilling.',
213293
},
294+
source: {
295+
code: `function TemperatureInput({ temperature }) {
296+
const [temp, setTemp] = useControl(temperature, 20);
297+
return (
298+
<div>
299+
<button onClick={() => setTemp(t => t - 1)}>-</button>
300+
<span>{temp}°C</span>
301+
<button onClick={() => setTemp(t => t + 1)}>+</button>
302+
</div>
303+
);
304+
}
305+
306+
function TemperatureCard({ temperature }) {
307+
const [temp] = useControl(temperature, 20);
308+
return (
309+
<div>
310+
<div>{temp}°C</div>
311+
<TemperatureInput temperature={temperature} />
312+
</div>
313+
);
314+
}
315+
316+
function TemperaturePanel({ temperature }) {
317+
return (
318+
<div>
319+
<TemperatureCard temperature={temperature} />
320+
<TemperatureCard temperature={temperature} />
321+
</div>
322+
);
323+
}
324+
325+
function App() {
326+
const [temp, setTemp, control] = useControl(20);
327+
return (
328+
<div>
329+
<div>Top-level: {temp}°C</div>
330+
<TemperaturePanel temperature={control} />
331+
<button onClick={() => setTemp(20)}>Reset</button>
332+
</div>
333+
);
334+
}`,
335+
language: 'tsx',
336+
},
214337
},
215338
},
216339
};

stories/transform.stories.tsx

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ function DoubleCounter() {
2222
<div style={{fontSize: 12, color: '#4a90d9', marginBottom: 8}}>Parent (useThru + mapSetter)</div>
2323
<div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
2424
<p style={{margin: 0, fontSize: 14, color: '#666'}}>
25-
<code>mapSetter(v =&gt; v * 2)</code> — every +1 click actually adds 2.
25+
<code>mapSetter(v =&gt; v * 2)</code> — the value written to state is doubled.
26+
Child calls <code>setState(n =&gt; n + 1)</code>, but the result is passed through <code>v * 2</code> before reaching state.
2627
</p>
2728
<div style={{padding: 8, border: '1px dashed #999', borderRadius: 4}}>
2829
<div style={{fontSize: 12, color: '#999', marginBottom: 4}}>Child</div>
@@ -168,7 +169,20 @@ export const Double: Story = {
168169
parameters: {
169170
docs: {
170171
description: {
171-
story: '`mapSetter` transforms the value before it reaches setState — every increment is doubled.',
172+
story: '`mapSetter` transforms the value before it reaches setState — the written value is doubled. E.g. `setState(n => n + 1)` with state `0` computes `1`, then `mapSetter` turns it into `1 * 2 = 2`.',
173+
},
174+
source: {
175+
code: `function DoubleCounter() {
176+
const [, , control] = useControl(0);
177+
const doubled = useThru(control, mapSetter(v => v * 2));
178+
return <SimpleCounter count={doubled} />;
179+
}
180+
181+
// Child calls setState(n => n + 1):
182+
// state 0 → (0+1) → mapSetter → 1*2 = 2
183+
// state 2 → (2+1) → mapSetter → 3*2 = 6
184+
// state 6 → (6+1) → mapSetter → 7*2 = 14`,
185+
language: 'tsx',
172186
},
173187
},
174188
},
@@ -181,6 +195,17 @@ export const Clamped: Story = {
181195
description: {
182196
story: '`mapSetter` with a clamp function to restrict the value range.',
183197
},
198+
source: {
199+
code: `function ClampedCounter() {
200+
const [, , control] = useControl(5);
201+
const clamped = useThru(
202+
control,
203+
mapSetter(v => Math.max(0, Math.min(10, v)))
204+
);
205+
return <ClampedDisplay count={clamped} />;
206+
}`,
207+
language: 'tsx',
208+
},
184209
},
185210
},
186211
};
@@ -193,6 +218,21 @@ export const MapState: Story = {
193218
description: {
194219
story: '`mapState` transforms the value that children see, without affecting the stored value.',
195220
},
221+
source: {
222+
code: `function MapStateDemo() {
223+
const [, , control] = useControl(0);
224+
// Child sees value * 100, but stored value is unchanged
225+
const scaled = useThru(control, mapState(v => v * 100));
226+
227+
return (
228+
<div>
229+
<SimpleCounter count={control} /> {/* raw: 0, 1, 2, ... */}
230+
<ScaledDisplay count={scaled} /> {/* scaled: 0, 100, 200, ... */}
231+
</div>
232+
);
233+
}`,
234+
language: 'tsx',
235+
},
196236
},
197237
},
198238
};
@@ -205,6 +245,24 @@ export const Watch: Story = {
205245
description: {
206246
story: '`watch` fires a side-effect callback whenever the value changes — useful for logging, analytics, or syncing.',
207247
},
248+
source: {
249+
code: `function WatchDemo() {
250+
const [logs, setLogs] = React.useState([]);
251+
const [, , control] = useControl(0);
252+
const watched = useThru(
253+
control,
254+
watch(v => setLogs(prev => [...prev.slice(-9), v]))
255+
);
256+
257+
return (
258+
<div>
259+
<SimpleCounter count={watched} />
260+
<div>{logs.map(v => \`onChange(\${v})\`)}</div>
261+
</div>
262+
);
263+
}`,
264+
language: 'tsx',
265+
},
208266
},
209267
},
210268
};
@@ -217,6 +275,21 @@ export const Chained: Story = {
217275
description: {
218276
story: 'Multiple `useThru` calls can be chained to compose transforms.',
219277
},
278+
source: {
279+
code: `function ChainedTransforms() {
280+
const [, , control] = useControl(0);
281+
const transformed = useThru(
282+
useThru(control, mapSetter(v => v + 10)),
283+
mapSetter(v => v * 2)
284+
);
285+
return <SimpleCounter count={transformed} />;
286+
}
287+
288+
// Child calls setState(n => n + 1):
289+
// state 0 → (0+1) → +10 → *2 = 22
290+
// state 22 → (22+1) → +10 → *2 = 66`,
291+
language: 'tsx',
292+
},
220293
},
221294
},
222295
};

0 commit comments

Comments
 (0)