Skip to content

Commit ee696d0

Browse files
authored
fix(react): form useForm hook (#62)
* fix(react): preserve FormInstance across re-renders in useForm hook useForm was creating a new FormInstance on every render instead of preserving it with useRef. This caused validation and field values to be lost when the parent component re-rendered. Also adds a multi-step registration form demo showcasing per-step validation with the form instance. * chore: relese note
1 parent 0c9711d commit ee696d0

5 files changed

Lines changed: 199 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tiny-design/react": patch
3+
---
4+
5+
fix: preserve FormInstance across re-renders in useForm hook
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import React, { useState } from 'react';
2+
import { Form, Input, InputNumber, Steps, Button, Flex, Result } from '@tiny-design/react';
3+
4+
const stepFields: string[][] = [
5+
['username', 'email'],
6+
['fullName', 'age', 'phone'],
7+
];
8+
9+
export default function StepFormDemo() {
10+
const [form] = Form.useForm({
11+
username: '',
12+
email: '',
13+
fullName: '',
14+
age: '',
15+
phone: '',
16+
});
17+
const [current, setCurrent] = useState(0);
18+
19+
const validateStep = (step: number): boolean => {
20+
const fields = stepFields[step];
21+
if (!fields) return true;
22+
fields.forEach((name) => form.validateField(name));
23+
return fields.every((name) => !form.getFieldError(name));
24+
};
25+
26+
const handleNext = () => {
27+
if (validateStep(current)) {
28+
setCurrent(current + 1);
29+
}
30+
};
31+
32+
const handlePrev = () => {
33+
setCurrent(current - 1);
34+
};
35+
36+
const handleFinish = () => {
37+
setCurrent(3);
38+
};
39+
40+
const handleReset = () => {
41+
form.resetFields();
42+
setCurrent(0);
43+
};
44+
45+
const stepStyle = (step: number): React.CSSProperties =>
46+
current !== step ? { display: 'none' } : {};
47+
48+
return (
49+
<div style={{ maxWidth: 600 }}>
50+
<Steps current={current} style={{ marginBottom: 24 }}>
51+
<Steps.Step title="Account" description="Login credentials" />
52+
<Steps.Step title="Profile" description="Personal info" />
53+
<Steps.Step title="Confirm" description="Review & submit" />
54+
<Steps.Step title="Done" description="Registration complete" />
55+
</Steps>
56+
57+
{current < 3 ? (
58+
<Form form={form} onFinish={handleFinish} noValidate>
59+
<div style={stepStyle(0)}>
60+
<Form.Item
61+
label="Username"
62+
name="username"
63+
rules={[
64+
{ required: true, message: 'Username is required' },
65+
{ min: 3, message: 'At least 3 characters' },
66+
]}
67+
>
68+
<Input placeholder="Enter username" />
69+
</Form.Item>
70+
<Form.Item
71+
label="Email"
72+
name="email"
73+
rules={[
74+
{ required: true, message: 'Email is required' },
75+
{
76+
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
77+
message: 'Please enter a valid email',
78+
},
79+
]}
80+
>
81+
<Input placeholder="Enter email" />
82+
</Form.Item>
83+
</div>
84+
85+
<div style={stepStyle(1)}>
86+
<Form.Item
87+
label="Full Name"
88+
name="fullName"
89+
rules={[{ required: true, message: 'Full name is required' }]}
90+
>
91+
<Input placeholder="Enter full name" />
92+
</Form.Item>
93+
<Form.Item
94+
label="Age"
95+
name="age"
96+
rules={[
97+
{ required: true, message: 'Age is required' },
98+
]}
99+
>
100+
<InputNumber min={1} max={120} placeholder="Enter age" />
101+
</Form.Item>
102+
<Form.Item
103+
label="Phone"
104+
name="phone"
105+
rules={[
106+
{ required: true, message: 'Phone number is required' },
107+
{ pattern: /^\d{7,15}$/, message: 'Please enter a valid phone number' },
108+
]}
109+
>
110+
<Input placeholder="Enter phone number" />
111+
</Form.Item>
112+
</div>
113+
114+
<div style={stepStyle(2)}>
115+
<Form.Item label="Username">
116+
<span>{form.getFieldValue('username')}</span>
117+
</Form.Item>
118+
<Form.Item label="Email">
119+
<span>{form.getFieldValue('email')}</span>
120+
</Form.Item>
121+
<Form.Item label="Full Name">
122+
<span>{form.getFieldValue('fullName')}</span>
123+
</Form.Item>
124+
<Form.Item label="Age">
125+
<span>{form.getFieldValue('age')}</span>
126+
</Form.Item>
127+
<Form.Item label="Phone">
128+
<span>{form.getFieldValue('phone')}</span>
129+
</Form.Item>
130+
</div>
131+
132+
<Form.Item>
133+
<Flex gap="sm">
134+
{current > 0 && (
135+
<Button type="button" onClick={handlePrev}>
136+
Previous
137+
</Button>
138+
)}
139+
{current < 2 && (
140+
<Button btnType="primary" type="button" onClick={handleNext}>
141+
Next
142+
</Button>
143+
)}
144+
{current === 2 && (
145+
<Button btnType="primary" type="submit">
146+
Submit
147+
</Button>
148+
)}
149+
</Flex>
150+
</Form.Item>
151+
</Form>
152+
) : (
153+
<Result
154+
status="success"
155+
title="Registration Successful!"
156+
subtitle={`Welcome, ${form.getFieldValue('fullName')}. Your account "${form.getFieldValue('username')}" has been created.`}
157+
extra={
158+
<Button btnType="primary" onClick={handleReset}>
159+
Register Another
160+
</Button>
161+
}
162+
/>
163+
)}
164+
</div>
165+
);
166+
}

packages/react/src/form/index.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import AsyncSubmitDemo from './demo/AsyncSubmit';
1616
import AsyncSubmitSource from './demo/AsyncSubmit.tsx?raw';
1717
import OtherControlsDemo from './demo/OtherControls';
1818
import OtherControlsSource from './demo/OtherControls.tsx?raw';
19+
import StepFormDemo from './demo/StepForm';
20+
import StepFormSource from './demo/StepForm.tsx?raw';
1921

2022
# Form
2123

@@ -124,6 +126,15 @@ A versatile example.
124126

125127
</Demo>
126128

129+
<Demo>
130+
### Multi-Step Form
131+
132+
A multi-step registration form. Each step validates its fields via the form instance before proceeding to the next step.
133+
134+
<DemoBlock component={StepFormDemo} source={StepFormSource} />
135+
136+
</Demo>
137+
127138
## API
128139

129140
### Form

packages/react/src/form/index.zh_CN.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import AsyncSubmitDemo from './demo/AsyncSubmit';
1616
import AsyncSubmitSource from './demo/AsyncSubmit.tsx?raw';
1717
import OtherControlsDemo from './demo/OtherControls';
1818
import OtherControlsSource from './demo/OtherControls.tsx?raw';
19+
import StepFormDemo from './demo/StepForm';
20+
import StepFormSource from './demo/StepForm.tsx?raw';
1921

2022
# Form
2123

@@ -121,6 +123,15 @@ const { Item, useForm, FormInstance } = Form;
121123

122124
<DemoBlock component={OtherControlsDemo} source={OtherControlsSource} />
123125

126+
<Demo>
127+
### 分步表单
128+
129+
多步骤注册表单。每一步通过表单实例校验当前步骤的字段后才能进入下一步。
130+
131+
<DemoBlock component={StepFormDemo} source={StepFormSource} />
132+
133+
</Demo>
134+
124135
## API
125136

126137
### Form
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import { useRef } from 'react';
12
import FormInstance, { FormValues } from './form-instance';
23

34
export default function useForm(initialValues: FormValues = {}): [FormInstance] {
4-
return [new FormInstance(initialValues)];
5+
const ref = useRef<FormInstance | null>(null);
6+
if (!ref.current) {
7+
ref.current = new FormInstance(initialValues);
8+
}
9+
return [ref.current];
510
}

0 commit comments

Comments
 (0)