第 7 章:表单
学习目标
- 理解受控组件 vs 非受控组件
- 掌握 input/textarea/select/checkbox 的写法
- 用 FormData + onSubmit 处理表单提交
- 避开表单的 5 个常见坑
一、受控 vs 非受控
受控组件(推荐):表单值由 React state 控制,输入触发 onChange 更新 state。
tsx
function Form() {
const [name, setName] = useState('');
return (
<input
value={name}
onChange={e => setName(e.target.value)}
/>
);
}非受控组件:用 ref 直接读 DOM 值,React 不管。
tsx
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
function handleSubmit() {
console.log(inputRef.current?.value); // ⚠️ 直接读 DOM
}
return <input ref={inputRef} />;
}⚠️ 坑 1:受控组件的 input 必须有
value和onChange,只设value会只读,输入框敲不动。
二、各种表单元素
text input
tsx
const [username, setUsername] = useState('');
<input value={username} onChange={e => setUsername(e.target.value)} />textarea
tsx
const [bio, setBio] = useState('');
<textarea value={bio} onChange={e => setBio(e.target.value)} />select(单选)
tsx
const [city, setCity] = useState('bj');
<select value={city} onChange={e => setCity(e.target.value)}>
<option value="bj">北京</option>
<option value="sh">上海</option>
<option value="sz">深圳</option>
</select>⚠️ 坑 2:React 用
value选中,不是selected属性。
checkbox
tsx
const [agree, setAgree] = useState(false);
<input
type="checkbox"
checked={agree}
onChange={e => setAgree(e.target.checked)}
/>radio
tsx
const [gender, setGender] = useState('m');
<label>
<input type="radio" value="m" checked={gender === 'm'} onChange={e => setGender(e.target.value)} />
男
</label>
<label>
<input type="radio" value="f" checked={gender === 'f'} onChange={e => setGender(e.target.value)} />
女
</label>file input(必须非受控)
tsx
<input type="file" ref={fileRef} />
function handle() { console.log(fileRef.current?.files); }⚠️ 坑 3:
type="file"必须用 ref,因为文件值不能由 JS 设置(浏览器安全限制)。
三、统一表单 state(对象)
tsx
interface Form {
username: string;
password: string;
remember: boolean;
gender: 'm' | 'f';
city: string;
}
function LoginForm() {
const [form, setForm] = useState<Form>({
username: '',
password: '',
remember: false,
gender: 'm',
city: 'bj',
});
function update<K extends keyof Form>(key: K, value: Form[K]) {
setForm(prev => ({ ...prev, [key]: value }));
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
console.log('提交', form);
}
return (
<form onSubmit={handleSubmit}>
<input value={form.username} onChange={e => update('username', e.target.value)} />
<input type="password" value={form.password} onChange={e => update('password', e.target.value)} />
<input type="checkbox" checked={form.remember} onChange={e => update('remember', e.target.checked)} />
<button type="submit">登录</button>
</form>
);
}四、表单验证
tsx
const [errors, setErrors] = useState<Record<string, string>>({});
function validate(): boolean {
const e: Record<string, string> = {};
if (!form.username) e.username = '用户名必填';
if (form.password.length < 6) e.password = '密码至少 6 位';
setErrors(e);
return Object.keys(e).length === 0;
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!validate()) return;
// 提交
}五、非受控 + FormData
适合大表单或简单场景:
tsx
function Upload() {
const formRef = useRef<HTMLFormElement>(null);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const data = new FormData(formRef.current!); // ⚠️ FormData 自动收集
fetch('/api/upload', { method: 'POST', body: data });
}
return (
<form ref={formRef} onSubmit={handleSubmit}>
<input name="title" />
<input name="file" type="file" />
<button>上传</button>
</form>
);
}⚠️ 坑 4:非受控输入必须加
name属性,FormData 通过name收集值。
六、第三方库(进阶)
复杂表单用 react-hook-form + zod,代码量减半:
tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
age: z.number().min(18),
});
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
</form>
);
}七、本章小结
| 要点 | 关键 |
|---|---|
| 受控组件 | value + onChange,输入框跟着 state 走 |
| 非受控组件 | ref 直接读 DOM,文件 input 必须非受控 |
| select | 用 value 不用 selected |
| checkbox | 用 checked + e.target.checked |
| 提交 | onSubmit + e.preventDefault() |
| FormData | 非受控表单自动收集,需 name 属性 |
| 复杂表单 | 推荐 react-hook-form + zod |
动手练习
- 登录表单:用户名 + 密码 + 记住我,提交时打印对象
- 注册表单:加邮箱、城市下拉、性别单选、协议勾选,统一存到 state 对象
- 校验:用户名为空报「必填」,密码小于 6 位报「至少 6 位」
- 找坑:以下代码为什么敲不进去?tsx
<input value="固定值" />
下一章:第 8 章:生命周期 →