第 153 章:表单与受控组件
学习目标
- 掌握受控组件与非受控组件
- 学会表单验证
- 理解 React Hook Form 优势
- 处理复杂表单场景
一、受控组件
表单值由 React state 控制。
tsx
'use client';
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
function handleSubmit(e: FormEvent) {
e.preventDefault();
console.log({ email, password });
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
/>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
/>
<button type="submit">登录</button>
</form>
);
}二、统一处理多个字段
2.1 用对象
tsx
interface FormData {
name: string;
email: string;
age: number;
}
function Form() {
const [form, setForm] = useState<FormData>({
name: '',
email: '',
age: 18,
});
function update<K extends keyof FormData>(field: K, value: FormData[K]) {
setForm(f => ({ ...f, [field]: value }));
}
return (
<form>
<input
value={form.name}
onChange={e => update('name', e.target.value)}
/>
<input
type="email"
value={form.email}
onChange={e => update('email', e.target.value)}
/>
<input
type="number"
value={form.age}
onChange={e => update('age', Number(e.target.value))}
/>
</form>
);
}三、验证
3.1 简单验证
tsx
function Form() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
function validate() {
if (!email) return '邮箱必填';
if (!/^.+@.+\..+$/.test(email)) return '邮箱格式错';
return '';
}
function handleSubmit(e: FormEvent) {
e.preventDefault();
const err = validate();
if (err) {
setError(err);
return;
}
setError('');
// 提交...
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={e => {
setEmail(e.target.value);
if (error) setError(''); // 输入时清错
}}
/>
{error && <p className="text-red-500">{error}</p>}
<button type="submit">提交</button>
</form>
);
}3.2 字段级错误
tsx
interface FormState {
values: Record<string, string>;
errors: Record<string, string>;
}
function useFormValidation<T extends Record<string, any>>(
initial: T,
validate: (values: T) => Record<keyof T, string>
) {
const [values, setValues] = useState<T>(initial);
const [errors, setErrors] = useState<Record<string, string>>({});
function update<K extends keyof T>(field: K, value: T[K]) {
setValues(v => ({ ...v, [field]: value }));
}
function handleSubmit(onValid: (v: T) => void) {
return (e: FormEvent) => {
e.preventDefault();
const errs = validate(values);
setErrors(errs);
if (Object.keys(errs).length === 0) onValid(values);
};
}
return { values, errors, update, handleSubmit };
}四、非受控组件(ref)
只在需要时取值,性能更好。
tsx
import { useRef } from 'react';
function UncontrolledForm() {
const nameRef = useRef<HTMLInputElement>(null);
const fileRef = useRef<HTMLInputElement>(null);
function handleSubmit(e: FormEvent) {
e.preventDefault();
const name = nameRef.current?.value;
const file = fileRef.current?.files?.[0];
console.log({ name, file });
}
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} type="text" />
<input ref={fileRef} type="file" />
<button type="submit">提交</button>
</form>
);
}五、React Hook Form(推荐)
bash
pnpm add react-hook-formtsx
'use client';
import { useForm } from 'react-hook-form';
interface FormData {
name: string;
email: string;
password: string;
}
function Signup() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
defaultValues: { name: '', email: '', password: '' },
});
async function onSubmit(data: FormData) {
await fetch('/api/signup', { method: 'POST', body: JSON.stringify(data) });
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('name', { required: '姓名必填', minLength: 2 })}
/>
{errors.name && <span>{errors.name.message}</span>}
<input
type="email"
{...register('email', {
required: '邮箱必填',
pattern: { value: /^.+@.+\..+$/, message: '格式错' },
})}
/>
{errors.email && <span>{errors.email.message}</span>}
<input
type="password"
{...register('password', {
required: '密码必填',
minLength: { value: 6, message: '至少 6 位' },
})}
/>
{errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : '注册'}
</button>
</form>
);
}5.1 优势
六、Zod 集成校验
bash
pnpm add zod @hookform/resolverstsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2, '至少 2 个字'),
email: z.string().email('邮箱格式错'),
age: z.number().min(0).max(150),
});
type FormData = z.infer<typeof schema>;
function Form() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(d => console.log(d))}>
<input {...register('name')} />
{errors.name?.message && <span>{errors.name.message}</span>}
<input {...register('email')} />
{errors.email?.message && <span>{errors.email.message}</span>}
<input type="number" {...register('age', { valueAsNumber: true })} />
{errors.age?.message && <span>{errors.age.message}</span>}
</form>
);
}七、本章小结
| 概念 | 关键 |
|---|---|
| 受控组件 | value + onChange |
| 非受控组件 | ref + defaultValue |
| 验证 | 同步或异步 |
| React Hook Form | 推荐,无重渲染 |
| Zod | schema 校验,类型推断 |
动手练习
- 写一个受控登录表单
- 写一个字段级错误提示
- 用 React Hook Form 重构上面的表单
- 用 Zod 实现注册表单校验
推荐阅读
- 📖 表单
- 📖 React Hook Form
- 📖 Zod