0%

React Hook Form 使用——表单验证与状态管理方案

React-hook-form 来处理表单,相比之前的 formik 和手写表单逻辑,使用体验相当棒!性能好、API 简洁、验证灵活,特别是 Controller 组件很好地解决了自定义组件集成的问题。

React Hook Form 简介

  React Hook Form 是一个高性能、灵活的表单验证库,使用 React Hooks 构建。它提供了最小化的 API,同时支持受控和非受控表单组件,专注于提供出色的用户体验和开发者体验。

  React Hook Form 的主要特点:

  1. 性能优秀: 最小的重新渲染,性能优于大多数表单库
  2. 灵活: 支持受控和非受控组件
  3. 轻量: Tree-shaking 支持,仅导入所需功能
  4. 验证友好: 支持多种验证策略和错误处理
  5. 类型安全: 完整的 Typescript 支持
  6. 易于测试: 简单的 API 便于单元测试

核心概念

1
2
3
4
5
6
7
8
// React Hook Form 的核心概念
// 1. register - 注册输入字段
// 2. handleSubmit - 处理表单提交
// 3. watch - 监听字段值变化
// 4. formState - 表单状态管理
// 5. setError - 设置表单错误
// 6. setValue - 设置字段值
// 7. trigger - 触发验证

安装和基础配置

安装 React Hook Form

1
2
3
4
5
6
# 安装核心库 npm install React-hook-form

# 或使用 yarn
yarn add React-hook-form

# 如果需要使用验证库 npm install React-hook-form@latest

基础使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import React from 'React';
import { useForm } from 'React-hook-form';

function BasicForm() {
const {
register,
handleSubmit,
watch,
formState: { errors, isSubmitting, isValid }
} = useForm({
mode: 'onChange', // 验证模式: onBlur, onChange, onSubmit, onTouched, all
reValidateMode: 'onChange', // 重新验证模式 defaultValues: {
firstName: '',
lastName: '',
email: '',
age: 18
},
shouldFocusError: true, // 验证失败时是否自动聚焦到错误字段 criteriaMode: 'firstError' // 'firstError' | 'all' - 错误收集模式
});

const onSubmit = async (data) => {
console.log('Form data:', data);

// 模拟异步提交 await new Promise(resolve => setTimeout(resolve, 1000));

alert('Form submitted successfully!');
};

// 监听字段变化 const watchedFirstName = watch('firstName');
const watchedFields = watch(); // 监听所有字段 return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="firstName">First Name *</label>
<input
id="firstName"
{...register('firstName', {
required: 'First name is required',
minLength: {
value: 2,
message: 'First name must be at least 2 characters'
},
maxLength: {
value: 20,
message: 'First name must be less than 20 characters'
}
})}
placeholder="Enter first name"
/>
{errors.firstName && (
<span style={{ color: 'red' }}>{errors.firstName.message}</span>
)}
</div>

<div>
<label htmlFor="lastName">Last Name *</label>
<input
id="lastName"
{...register('lastName', {
required: 'Last name is required',
validate: 2024-06-09 10:00:00
if (value === 'admin') {
return 'Last name cannot be admin';
}
return true;
}
})}
placeholder="Enter last name"
/>
{errors.lastName && (
<span style={{ color: 'red' }}>{errors.lastName.message}</span>
)}
</div>

<div>
<label htmlFor="email">Email *</label>
<input
id="email"
type="email"
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
})}
placeholder="Enter email"
/>
{errors.email && (
<span style={{ color: 'red' }}>{errors.email.message}</span>
)}
</div>

<div>
<label htmlFor="age">Age</label>
<input
id="age"
type="number"
{...register('age', {
min: {
value: 18,
message: 'Must be at least 18 years old'
},
max: {
value: 100,
message: 'Must be less than 100 years old'
}
})}
/>
{errors.age && (
<span style={{ color: 'red' }}>{errors.age.message}</span>
)}
</div>

<div>
<label>
<input
type="checkbox"
{...register('agreed', { required: 'You must agree to terms' })}
/>
I agree to the terms and conditions *
</label>
{errors.agreed && (
<span style={{ color: 'red' }}>{errors.agreed.message}</span>
)}
</div>

<div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</div>

{/* 显示表单状态 */}
<div style={{ marginTop: '20px' }}>
<p>Watched First Name: {watchedFirstName}</p>
<p>Form Valid: {isValid.toString()}</p>
<p>Is Submitting: {isSubmitting.toString()}</p>
</div>
</form>
);
}

export default BasicForm;

验证模式详解

不同验证模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { useForm } from 'React-hook-form';

function ValidationModes() {
const {
register,
handleSubmit,
formState: { errors, dirtyFields, touchedFields }
} = useForm({
mode: 'onChange', // 最常用: 每次输入时验证
// mode: 'onBlur', // 失去焦点时验证
// mode: 'onSubmit', // 提交时验证 (默认)
// mode: 'onTouched', // 首次交互后验证
// mode: 'all', // onTouched + onChange
});

const onSubmit = (data) => {
console.log('Form data:', data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email'
}
})}
placeholder="Email (validated onChange)"
/>
{errors.email && <span>{errors.email.message}</span>}
</div>

<button type="submit">Submit</button>
</form>
);
}

高级验证策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import { useForm } from 'React-hook-form';
import * as yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';

// 使用 Yup 进行复杂验证 const schema = yup.object({
username: yup
.string()
.required('Username is required')
.min(3, 'Username must be at least 3 characters')
.max(20, 'Username must be less than 20 characters')
.matches(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),

email: yup
.string()
.required('Email is required')
.email('Invalid email format'),

password: yup
.string()
.required('Password is required')
.min(8, 'Password must be at least 8 characters')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Password must contain uppercase, lowercase, and number'),

confirmPassword: yup
.string()
.oneOf([yup.ref('password')], 'Passwords must match')
.required('Confirm password is required'),

age: yup
.number()
.nullable()
.min(18, 'Must be at least 18 years old')
.max(120, 'Must be less than 120 years old'),

website: yup
.string()
.nullable()
.url('Must be a valid URL'),

tags: yup
.array()
.of(yup.string().required())
.min(1, 'At least one tag is required'),

// 条件验证 phoneNumber: yup
.string()
.when('country', {
is: 'US',
then: (schema) => schema.matches(/^\d{10}$/, 'Must be 10 digits for US'),
otherwise: (schema) => schema.matches(/^\d{7,15}$/, 'Must be 7-15 digits')
})
});

function AdvancedValidationForm() {
const {
register,
handleSubmit,
formState: { errors, isValid },
watch,
setValue,
trigger
} = useForm({
resolver: yupResolver(schema),
mode: 'onChange',
defaultValues: {
username: '',
email: '',
password: '',
confirmPassword: '',
age: null,
website: '',

tags: [],
country: 'US',
phoneNumber: ''
}
});

const watchedCountry = watch('country');

const onSubmit = (data) => {
console.log('Validated data:', data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('username')}
placeholder="Username"
/>
{errors.username && <span>{errors.username.message}</span>}
</div>

<div>
<input
{...register('email')}
type="email"
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
</div>

<div>
<input
{...register('password')}
type="password"
placeholder="Password"
/>
{errors.password && <span>{errors.password.message}</span>}
</div>

<div>
<input
{...register('confirmPassword')}
type="password"
placeholder="Confirm Password"
/>
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
</div>

<div>
<input
{...register('age')}
type="number"
placeholder="Age"
/>
{errors.age && <span>{errors.age.message}</span>}
</div>

<div>
<input
{...register('website')}
placeholder="Website (optional)"
/>
{errors.website && <span>{errors.website.message}</span>}
</div>

<div>
<select {...register('country')}>
<option value="US">United States</option>
<option value="UK">United Kingdom</option>
<option value="CA">Canada</option>
</select>
</div>

<div>
<input
{...register('phoneNumber')}
placeholder="Phone Number"
/>
{errors.phoneNumber && <span>{errors.phoneNumber.message}</span>}
</div>

<div>
<button type="button" onClick={() => setValue('country', 'UK')}>
Change to UK
</button>
<button type="button" onClick={() => trigger()}>
Validate All Fields
</button>
</div>

<button type="submit" disabled={!isValid}>
Submit
</button>
</form>
);
}

Controller 组件使用

基础 Controller 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import React from 'React';
import { useForm, Controller } from 'React-hook-form';
import { TextField, Checkbox, FormControlLabel, MenuItem, Select } from '@mui/material';

function ControllerExample() {
const { control, handleSubmit, formState: { errors } } = useForm({
defaultValues: {
text: '',
checkbox: false,
select: '',
radio: '',
multiline: ''
}
});

const onSubmit = (data) => {
console.log('Controller form data:', data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="text"
control={control}
rules={{ required: 'Text is required' }}
render={({ field }) => (
<TextField
{...field}
label="Text Field"
error={!!errors.text}
helperText={errors.text?.message}
fullWidth
margin="normal"
/>
)}
/>

<Controller
name="checkbox"
control={control}
rules={{ required: 'Checkbox is required' }}
render={({ field }) => (
<FormControlLabel
control={
<Checkbox
{...field}
checked={field.value}
/>
}
label="Accept Terms"
/>
)}
/>
{errors.checkbox && <span style={{ color: 'red' }}>{errors.checkbox.message}</span>}

<Controller
name="select"
control={control}
rules={{ required: 'Selection is required' }}
render={({ field }) => (
<Select {...field} displayEmpty>
<MenuItem value="">
<em>Select an option</em>
</MenuItem>
<MenuItem value="option1">Option 1</MenuItem>
<MenuItem value="option2">Option 2</MenuItem>
<MenuItem value="option3">Option 3</MenuItem>
</Select>
)}
/>

<button type="submit">Submit</button>
</form>
);
}

自定义组件集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import React, { forwardRef } from 'React';
import { useForm, Controller } from 'React-hook-form';

// 自定义输入组件 const CustomInput = forwardRef(({ value, onChange, onBlur, ...props }, ref) => {
return (
<div style={{ position: 'relative' }}>
<input
ref={ref}
value={value}
onChange={onChange}
onBlur={onBlur}
{...props}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
/>
</div>
);
});

// 自定义选择器组件 const CustomSelect = forwardRef(({ value, onChange, options, ...props }, ref) => {
return (
<select
ref={ref}
value={value}
onChange={onChange}
{...props}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
>
{options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
});

function CustomComponentForm() {
const { control, handleSubmit } = useForm({
defaultValues: {
customInput: '',
customSelect: '',
customMultiSelect: []
}
});

const onSubmit = (data) => {
console.log('Custom component form data:', data);
};

const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' }
];

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="customInput"
control={control}
rules={{ required: 'Custom input is required' }}
render={({ field, fieldState }) => (
<>
<CustomInput {...field} placeholder="Custom input" />
{fieldState.error && (
<span style={{ color: 'red', fontSize: '12px' }}>
{fieldState.error.message}
</span>
)}
</>
)}
/>

<Controller
name="customSelect"
control={control}
rules={{ required: 'Selection is required' }}
render={({ field, fieldState }) => (
<>
<CustomSelect
{...field}
options={options}
/>
{fieldState.error && (
<span style={{ color: 'red', fontSize: '12px' }}>
{fieldState.error.message}
</span>
)}
</>
)}
/>

<button type="submit">Submit</button>
</form>
);
}

高级功能

字段数组管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { useFieldArray, useForm } from 'React-hook-form';

function FieldArrayExample() {
const { control, register, handleSubmit, formState: { errors } } = useForm({
defaultValues: {
emails: [{ email: '' }],
skills: [{ name: '', level: '' }]
}
});

const {
fields: emailFields,
append: appendEmail,
remove: removeEmail,
insert: insertEmail
} = useFieldArray({
control,
name: 'emails'
});

const {
fields: skillFields,
append: appendSkill,
remove: removeSkill
} = useFieldArray({
control,
name: 'skills'
});

const onSubmit = (data) => {
console.log('Form with arrays:', data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<h3>Emails</h3>
{emailFields.map((field, index) => (
<div key={field.id} style={{ marginBottom: '10px' }}>
<input
{...register(`emails.${index}.email`, {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email'
}
})}
placeholder="Email"
/>
{errors.emails?.[index]?.email && (
<span style={{ color: 'red' }}>
{errors.emails?.[index]?.email?.message}
</span>
)}
<button
type="button"
onClick={() => removeEmail(index)}
style={{ marginLeft: '10px' }}
>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => appendEmail({ email: '' })}
>
Add Email
</button>
</div>

<div>
<h3>Skills</h3>
{skillFields.map((field, index) => (
<div key={field.id} style={{ marginBottom: '10px' }}>
<input
{...register(`skills.${index}.name`, {
required: 'Skill name is required'
})}
placeholder="Skill name"
/>
<select
{...register(`skills.${index}.level`, {
required: 'Skill level is required'
})}
>
<option value="">Select level</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
{(errors.skills?.[index]?.name || errors.skills?.[index]?.level) && (
<span style={{ color: 'red' }}>
{errors.skills?.[index]?.name?.message || errors.skills?.[index]?.level?.message}
</span>
)}
<button
type="button"
onClick={() => removeSkill(index)}
style={{ marginLeft: '10px' }}
>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => appendSkill({ name: '', level: '' })}
>
Add Skill
</button>
</div>

<button type="submit">Submit</button>
</form>
);
}

动态表单和条件字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { useForm, useWatch } from 'React-hook-form';

function DynamicForm() {
const { register, control, handleSubmit, formState: { errors } } = useForm({
defaultValues: {
userType: 'basic',
username: '',
email: '',
// Basic user fields
department: '',
// Premium user fields
subscriptionType: '',
billingAddress: '',
paymentMethod: 'credit_card'
}
});

const userType = useWatch({
control,
name: 'userType'
});

const onSubmit = (data) => {
console.log('Dynamic form data:', data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>
<input
type="radio"
value="basic"
{...register('userType')}
/>
Basic User
</label>
<label>
<input
type="radio"
value="premium"
{...register('userType')}
/>
Premium User
</label>
</div>

<div>
<input
{...register('username', {
required: 'Username is required'
})}
placeholder="Username"
/>
{errors.username && <span>{errors.username.message}</span>}
</div>

<div>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email'
}
})}
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
</div>

{userType === 'basic' && (
<div>
<input
{...register('department', {
required: 'Department is required'
})}
placeholder="Department"
/>
{errors.department && <span>{errors.department.message}</span>}
</div>
)}

{userType === 'premium' && (
<>
<div>
<select {...register('subscriptionType', {
required: 'Subscription type is required'
})}>
<option value="">Select subscription</option>
<option value="monthly">Monthly</option>
<option value="annual">Annual</option>
</select>
{errors.subscriptionType && <span>{errors.subscriptionType.message}</span>}
</div>

<div>
<textarea
{...register('billingAddress', {
required: 'Billing address is required'
})}
placeholder="Billing Address"
rows={4}
/>
{errors.billingAddress && <span>{errors.billingAddress.message}</span>}
</div>

<div>
<label>
<input
type="radio"
value="credit_card"
{...register('paymentMethod')}
/>
Credit Card
</label>
<label>
<input
type="radio"
value="paypal"
{...register('paymentMethod')}
/>
PayPal
</label>
</div>
</>
)}

<button type="submit">Submit</button>
</form>
);
}

自定义验证器和工具函数

验证器工具函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// validation-utils.JS
import { isEmail } from 'validator';

// 通用验证函数 export const validators = {
// 基础验证 required: (message = 'This field is required') => ({
validate: 2024-06-09 10:00:00
if (typeof value === 'string') {
return value.trim() !== '' || message;
}
return value != null || message;
}
}),

email: (message = 'Invalid email address') => ({
validate: 2024-06-09 10:00:00
}),

minLength: (min, message) => ({
minLength: {
value: min,
message: message || `Must be at least ${min} characters`
}
}),

maxLength: (max, message) => ({
maxLength: {
value: max,
message: message || `Must be less than ${max} characters`
}
}),

min: (min, message) => ({
min: {
value: min,
message: message || `Must be at least ${min}`
}
}),

max: (max, message) => ({
max: {
value: max,
message: message || `Must be less than ${max}`
}
}),

pattern: (regex, message) => ({
pattern: {
value: regex,
message: message || 'Invalid format'
}
}),

// 自定义验证 password: (message = 'Password must meet requirements') => ({
validate: 2024-06-09 10:00:00
if (!value) return true;

const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasNumbers = /\d/.test(value);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(value);
const isValidLength = value.length >= 8;

return (
hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasSpecialChar &&
isValidLength
) || message;
}
}),

phone: (message = 'Invalid phone number') => ({
validate: 2024-06-09 10:00:00
if (!value) return true;
const phoneRegex = /^(\+\d{1,3}[- ]?)?\d{10}$/;
return phoneRegex.test(value) || message;
}
}),

url: (message = 'Invalid URL format') => ({
validate: 2024-06-09 10:00:00
if (!value) return true;
try {
new URL(value);
return true;
} catch {
return message;
}
}
}),

// 比较验证 compareWith: (otherFieldName, operator, message) => ({
validate: 2024-06-09 10:00:00
if (value == null || formValues[otherFieldName] == null) return true;

switch (operator) {
case 'equal':
return value === formValues[otherFieldName] || message;
case 'notEqual':
return value !== formValues[otherFieldName] || message;
case 'greaterThan':
return Number(value) > Number(formValues[otherFieldName]) || message;
case 'lessThan':
return Number(value) < Number(formValues[otherFieldName]) || message;
default:
return true;
}
}
})
};

// 验证器组合函数 export const composeValidators = (...validators) => {
return (value) => {
for (const validator of validators) {
const result = validator(value);
if (result !== true) {
return result;
}
}
return true;
};
};

实际应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import { useForm } from 'React-hook-form';
import { validators } from './validation-utils';

function ValidationUtilsExample() {
const { register, handleSubmit, formState: { errors } } = useForm({
defaultValues: {
email: '',
password: '',
confirmPassword: '',
age: '',
website: '',
phone: ''
}
});

const onSubmit = (data) => {
console.log('Validated data:', data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('email', validators.email('Please enter a valid email'))}
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
</div>

<div>
<input
type="password"
{...register('password', validators.password())}
placeholder="Password"
/>
{errors.password && <span>{errors.password.message}</span>}
</div>

<div>
<input
type="password"
{...register('confirmPassword', {
...validators.required(),
validate: 2024-06-09 10:00:00
value === watch('password') || 'Passwords do not match'
})}
placeholder="Confirm Password"
/>
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
</div>

<div>
<input
type="number"
{...register('age', validators.min(18, 'Must be at least 18 years old'))}
placeholder="Age"
/>
{errors.age && <span>{errors.age.message}</span>}
</div>

<div>
<input
{...register('website', validators.url('Please enter a valid URL'))}
placeholder="Website"
/>
{errors.website && <span>{errors.website.message}</span>}
</div>

<div>
<input
{...register('phone', validators.phone('Please enter a valid phone number'))}
placeholder="Phone"
/>
{errors.phone && <span>{errors.phone.message}</span>}
</div>

<button type="submit">Submit</button>
</form>
);
}

性能优化和最佳实践

性能优化策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import React, { memo } from 'React';
import { useForm, Controller } from 'React-hook-form';

// 优化:避免不必要的重新渲染 const OptimizedInput = memo(({ field, fieldState, label, type = 'text', ...props }) => {
return (
<div>
<label>{label}</label>
<input
{...field}
type={type}
{...props}
/>
{fieldState.error && (
<span style={{ color: 'red' }}>{fieldState.error.message}</span>
)}
</div>
);
});

function OptimizedForm() {
const { control, handleSubmit } = useForm({
defaultValues: {
field1: '',
field2: '',
field3: '',
field4: ''
},
mode: 'onChange',
reValidateMode: 'onChange'
});

const onSubmit = (data) => {
console.log('Optimized form data:', data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="field1"
control={control}
rules={{ required: 'Field 1 is required' }}
render={({ field, fieldState }) => (
<OptimizedInput
field={field}
fieldState={fieldState}
label="Field 1"
/>
)}
/>

<Controller
name="field2"
control={control}
rules={{ required: 'Field 2 is required' }}
render={({ field, fieldState }) => (
<OptimizedInput
field={field}
fieldState={fieldState}
label="Field 2"
/>
)}
/>

<button type="submit">Submit</button>
</form>
);
}

表单重置和清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { useForm } from 'React-hook-form';

function ResetForm() {
const { register, handleSubmit, reset, formState, control } = useForm({
defaultValues: {
name: '',
email: '',
message: ''
},
mode: 'onChange'
});

const onSubmit = (data) => {
console.log('Form data:', data);

// 提交成功后重置表单 reset();
};

const handleReset = () => {
// 重置为默认值 reset();

// 或重置为特定值
// reset({
// name: 'Default Name',
// email: 'default@example.com',
// message: ''
// });

// 重置表单状态(不重置值)
// resetField('name');
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('name', { required: 'Name is required' })}
placeholder="Name"
/>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email'
}
})}
placeholder="Email"
/>
<textarea
{...register('message')}
placeholder="Message"
rows={4}
/>

<div>
<button type="submit">Submit</button>
<button type="button" onClick={handleReset} style={{ marginLeft: '10px' }}>
Reset
</button>
</div>
</form>
);
}

实际应用案例

用户注册表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
import React from 'React';
import { useForm, Controller } from 'React-hook-form';
import { TextField, Button, Box, Typography, FormControlLabel, Switch } from '@mui/material';

function UserRegistrationForm() {
const {
control,
handleSubmit,
formState: { errors, isValid, isSubmitting },
watch,
setValue
} = useForm({
mode: 'onChange',
defaultValues: {
username: '',
email: '',
password: '',
confirmPassword: '',
firstName: '',
lastName: '',
age: '',
newsletter: false,
privacyPolicy: false
}
});

const password = watch('password');
const newsletter = watch('newsletter');

const onSubmit = async (data) => {
console.log('Registration data:', data);

// 模拟 API 调用 try {
// const response = await fetch('/API/register', {
// method: 'POST',
// headers: { 'Content-Type': 'application/Json' },
// body: Json.stringify(data)
// });

// 模拟延迟 await new Promise(resolve => setTimeout(resolve, 2000));

alert('Registration successful!');
} catch (error) {
console.error('Registration failed:', error);
alert('Registration failed. Please try again.');
}
};

const validatePasswordMatch = (value) => {
return value === password || 'Passwords do not match';
};

const validateAge = (value) => {
const age = parseInt(value);
if (isNaN(age)) return 'Please enter a valid age';
if (age < 13) return 'You must be at least 13 years old';
if (age > 120) return 'Please enter a valid age';
return true;
};

return (
<Box sx={{ maxWidth: 600, mx: 'auto', p: 3 }}>
<Typography variant="h4" gutterBottom>
User Registration
</Typography>

<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="username"
control={control}
rules={{
required: 'Username is required',
minLength: { value: 3, message: 'Username must be at least 3 characters' },
maxLength: { value: 20, message: 'Username must be less than 20 characters' },
pattern: {
value: /^[a-zA-Z0-9_]+$/,
message: 'Username can only contain letters, numbers, and underscores'
}
}}
render={({ field }) => (
<TextField
{...field}
label="Username *"
fullWidth
margin="normal"
error={!!errors.username}
helperText={errors.username?.message}
disabled={isSubmitting}
/>
)}
/>

<Controller
name="email"
control={control}
rules={{
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
}}
render={({ field }) => (
<TextField
{...field}
label="Email *"
type="email"
fullWidth
margin="normal"
error={!!errors.email}
helperText={errors.email?.message}
disabled={isSubmitting}
/>
)}
/>

<Controller
name="password"
control={control}
rules={{
required: 'Password is required',
minLength: { value: 8, message: 'Password must be at least 8 characters' },
pattern: {
value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
message: 'Password must contain uppercase, lowercase, number and special character'
}
}}
render={({ field }) => (
<TextField
{...field}
label="Password *"
type="password"
fullWidth
margin="normal"
error={!!errors.password}
helperText={errors.password?.message}
disabled={isSubmitting}
/>
)}
/>

<Controller
name="confirmPassword"
control={control}
rules={{
required: 'Please confirm your password',
validate: 2024-06-09 10:00:00
}}
render={({ field }) => (
<TextField
{...field}
label="Confirm Password *"
type="password"
fullWidth
margin="normal"
error={!!errors.confirmPassword}
helperText={errors.confirmPassword?.message}
disabled={isSubmitting}
/>
)}
/>

<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
<Controller
name="firstName"
control={control}
rules={{ required: 'First name is required' }}
render={({ field }) => (
<TextField
{...field}
label="First Name *"
fullWidth
margin="normal"
error={!!errors.firstName}
helperText={errors.firstName?.message}
disabled={isSubmitting}
/>
)}
/>

<Controller
name="lastName"
control={control}
rules={{ required: 'Last name is required' }}
render={({ field }) => (
<TextField
{...field}
label="Last Name *"
fullWidth
margin="normal"
error={!!errors.lastName}
helperText={errors.lastName?.message}
disabled={isSubmitting}
/>
)}
/>
</Box>

<Controller
name="age"
control={control}
rules={{
required: 'Age is required',
validate: 2024-06-09 10:00:00
}}
render={({ field }) => (
<TextField
{...field}
label="Age *"
type="number"
fullWidth
margin="normal"
error={!!errors.age}
helperText={errors.age?.message}
disabled={isSubmitting}
/>
)}
/>

<Controller
name="newsletter"
control={control}
render={({ field }) => (
<FormControlLabel
control={
<Switch
{...field}
checked={field.value}
disabled={isSubmitting}
/>
}
label="Subscribe to newsletter"
/>
)}
/>

{newsletter && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
You'll receive updates about new features and offers.
</Typography>
)}

<Controller
name="privacyPolicy"
control={control}
rules={{ required: 'You must agree to the privacy policy' }}
render={({ field }) => (
<FormControlLabel
control={
<Switch
{...field}
checked={field.value}
disabled={isSubmitting}
/>
}
label="I agree to the Privacy Policy *"
/>
)}
/>
{errors.privacyPolicy && (
<Typography variant="caption" color="error" sx={{ ml: 2 }}>
{errors.privacyPolicy.message}
</Typography>
)}

<Box sx={{ mt: 3 }}>
<Button
type="submit"
variant="contained"
size="large"
disabled={isSubmitting || !isValid}
fullWidth
>
{isSubmitting ? 'Registering...' : 'Register'}
</Button>
</Box>
</form>
</Box>
);
}

最佳实践总结

1. 表单架构设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// forms/constants.JS
export const FORM_MODES = {
CREATE: 'create',
EDIT: 'edit',
VIEW: 'view'
};

export const VALIDATION_MESSAGES = {
REQUIRED: 'This field is required',
INVALID_EMAIL: 'Please enter a valid email address',
PASSWORD_REQUIREMENTS: 'Password must meet complexity requirements',
MIN_LENGTH: (min) => `Must be at least ${min} characters`,
MAX_LENGTH: (max) => `Must be less than ${max} characters`
};

2. 可复用表单组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// components/ReusableForm.jsx
import { useForm } from 'React-hook-form';

export function ReusableForm({
defaultValues = {},
validationRules = {},
onSubmit,
children,
mode = 'onChange'
}) {
const methods = useForm({
defaultValues,
mode,
reValidateMode: 'onChange'
});

const handleSubmit = methods.handleSubmit(onSubmit);

return (
<form onSubmit={handleSubmit}>
{children(methods)}
</form>
);
}

// 使用示例 function ContactForm() {
return (
<ReusableForm
defaultValues={{ name: '', email: '', message: '' }}
validationRules={{
name: { required: true },
email: { required: true, pattern: /^\S+@\S+$/i },
message: { required: true }
}}
onSubmit={handleContactSubmit}
>
{({ register, formState: { errors } }) => (
<>
<input {...register('name')} placeholder="Name" />
{errors.name && <span>{errors.name.message}</span>}

<input {...register('email')} placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}

<textarea {...register('message')} placeholder="Message" />
{errors.message && <span>{errors.message.message}</span>}

<button type="submit">Send</button>
</>
)}
</ReusableForm>
);
}

总结

  • React Hook Form 提供了高性能、灵活的表单处理方案
  • 支持受控和非受控组件,满足不同场景需求
  • 验证功能强大且灵活,支持多种验证策略
  • Controller 组件很好地集成了自定义组件
  • 字段数组功能便于处理动态表单项
  • 性能优秀,最小化不必要的重新渲染
  • 完整的 Typescript 支持

从其他表单库迁移到 React-hook-form 后,表单处理变得简单高效。特别是 Controller 组件,让自定义 UI 组件的集成变得非常容易。

扩展阅读

  • React Hook Form Official Documentation
  • Form State Management Patterns
  • Performance Optimization Guide
  • Validation Best Practices
  • Typescript Integration Guide

参考资料

  • React Hook Form GitHub: https://github.com/React-hook-form/React-hook-form
  • Form Validation Libraries Comparison: https://github.com/igorhalfeld/awesome-React-form-libraries
  • React Form Best Practices: https://reactjs.org/docs/forms.Html
bulb