Skip to content

Commit cd808b8

Browse files
committed
fix: input not persist after rerender
1 parent a543be5 commit cd808b8

3 files changed

Lines changed: 82 additions & 3 deletions

File tree

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"Bash(git commit:*)",
1616
"Bash(bun tsc:*)",
1717
"Bash(ls:*)",
18-
"Bash(bun x tsc:*)"
18+
"Bash(bun x tsc:*)",
19+
"Bash(bun:*)"
1920
],
2021
"deny": []
2122
}

src/reconciler.test.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,60 @@ describe('Telegram Reconciler', () => {
324324
container.inputCallbacks[0]?.callback('test message');
325325
expect(onSubmit).toHaveBeenCalledWith('test message');
326326
});
327+
328+
it('should preserve input elements across re-renders', async () => {
329+
const { container, render, clickButton } = createContainer();
330+
331+
const App = () => {
332+
const [mode, setMode] = useState<'normal' | 'secret'>('normal');
333+
const handleNormal = vi.fn();
334+
const handleSecret = vi.fn();
335+
336+
return (
337+
<>
338+
{mode === 'normal' ? (
339+
<input onSubmit={handleNormal} />
340+
) : (
341+
<input onSubmit={handleSecret} autoDelete />
342+
)}
343+
<row>
344+
<button onClick={() => setMode(m => m === 'normal' ? 'secret' : 'normal')}>
345+
Toggle
346+
</button>
347+
</row>
348+
</>
349+
);
350+
};
351+
352+
render(<App />);
353+
await new Promise(resolve => setTimeout(resolve, 0));
354+
355+
// Initial state - normal mode
356+
expect(container.root.children[0]).toEqual({
357+
type: 'input',
358+
onSubmit: expect.any(Function),
359+
autoDelete: undefined
360+
});
361+
expect(container.inputCallbacks).toHaveLength(1);
362+
expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined();
363+
364+
// Toggle to secret mode
365+
clickButton('0-0');
366+
await new Promise(resolve => setTimeout(resolve, 0));
367+
368+
// Should still have input but with different props
369+
expect(container.root.children[0]).toEqual({
370+
type: 'input',
371+
onSubmit: expect.any(Function),
372+
autoDelete: true
373+
});
374+
expect(container.inputCallbacks).toHaveLength(1);
375+
expect(container.inputCallbacks[0]?.autoDelete).toBe(true);
376+
377+
// Verify the callback works
378+
container.inputCallbacks[0]?.callback('test');
379+
380+
// Verify the structure is correct
381+
expect(container.inputCallbacks[0]).toBeDefined();
382+
});
327383
});

src/reconciler.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,31 @@ const hostConfig: ReactReconciler.HostConfig<
215215
) {
216216
// Deep clone but preserve functions
217217
const clone = JSON.parse(JSON.stringify(instance));
218-
if (instance.onClick) {
219-
clone.onClick = instance.onClick;
218+
219+
// Update with new props for specific instance types
220+
if (instance.type === 'button') {
221+
clone.onClick = newProps.onClick || instance.onClick;
222+
// Update button text from new props if available
223+
if (newProps.children !== undefined) {
224+
let buttonText = '';
225+
if (typeof newProps.children === 'string') {
226+
buttonText = newProps.children;
227+
} else if (Array.isArray(newProps.children)) {
228+
buttonText = newProps.children.map((child: any) =>
229+
typeof child === 'string' ? child : String(child)
230+
).join('');
231+
} else if (newProps.children != null) {
232+
buttonText = String(newProps.children);
233+
}
234+
clone.text = buttonText;
235+
}
236+
} else if (instance.type === 'input') {
237+
clone.onSubmit = newProps.onSubmit;
238+
clone.autoDelete = newProps.autoDelete;
239+
} else if (instance.type === 'link') {
240+
clone.href = newProps.href || instance.href;
220241
}
242+
221243
// Handle children based on keepChildren flag
222244
if (clone.children && Array.isArray(clone.children)) {
223245
if (keepChildren) {

0 commit comments

Comments
 (0)