fix: input not persist after rerender

This commit is contained in:
Kyle Fang
2025-07-01 09:30:49 +08:00
parent a543be55f0
commit cd808b8d4f
3 changed files with 82 additions and 3 deletions

View File

@@ -15,7 +15,8 @@
"Bash(git commit:*)",
"Bash(bun tsc:*)",
"Bash(ls:*)",
"Bash(bun x tsc:*)"
"Bash(bun x tsc:*)",
"Bash(bun:*)"
],
"deny": []
}

View File

@@ -324,4 +324,60 @@ describe('Telegram Reconciler', () => {
container.inputCallbacks[0]?.callback('test message');
expect(onSubmit).toHaveBeenCalledWith('test message');
});
it('should preserve input elements across re-renders', async () => {
const { container, render, clickButton } = createContainer();
const App = () => {
const [mode, setMode] = useState<'normal' | 'secret'>('normal');
const handleNormal = vi.fn();
const handleSecret = vi.fn();
return (
<>
{mode === 'normal' ? (
<input onSubmit={handleNormal} />
) : (
<input onSubmit={handleSecret} autoDelete />
)}
<row>
<button onClick={() => setMode(m => m === 'normal' ? 'secret' : 'normal')}>
Toggle
</button>
</row>
</>
);
};
render(<App />);
await new Promise(resolve => setTimeout(resolve, 0));
// Initial state - normal mode
expect(container.root.children[0]).toEqual({
type: 'input',
onSubmit: expect.any(Function),
autoDelete: undefined
});
expect(container.inputCallbacks).toHaveLength(1);
expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined();
// Toggle to secret mode
clickButton('0-0');
await new Promise(resolve => setTimeout(resolve, 0));
// Should still have input but with different props
expect(container.root.children[0]).toEqual({
type: 'input',
onSubmit: expect.any(Function),
autoDelete: true
});
expect(container.inputCallbacks).toHaveLength(1);
expect(container.inputCallbacks[0]?.autoDelete).toBe(true);
// Verify the callback works
container.inputCallbacks[0]?.callback('test');
// Verify the structure is correct
expect(container.inputCallbacks[0]).toBeDefined();
});
});

View File

@@ -215,9 +215,31 @@ const hostConfig: ReactReconciler.HostConfig<
) {
// Deep clone but preserve functions
const clone = JSON.parse(JSON.stringify(instance));
if (instance.onClick) {
clone.onClick = instance.onClick;
// Update with new props for specific instance types
if (instance.type === 'button') {
clone.onClick = newProps.onClick || instance.onClick;
// Update button text from new props if available
if (newProps.children !== undefined) {
let buttonText = '';
if (typeof newProps.children === 'string') {
buttonText = newProps.children;
} else if (Array.isArray(newProps.children)) {
buttonText = newProps.children.map((child: any) =>
typeof child === 'string' ? child : String(child)
).join('');
} else if (newProps.children != null) {
buttonText = String(newProps.children);
}
clone.text = buttonText;
}
} else if (instance.type === 'input') {
clone.onSubmit = newProps.onSubmit;
clone.autoDelete = newProps.autoDelete;
} else if (instance.type === 'link') {
clone.href = newProps.href || instance.href;
}
// Handle children based on keepChildren flag
if (clone.children && Array.isArray(clone.children)) {
if (keepChildren) {