讲解
React 表单有两种模式。受控组件(主流):输入框的值由 state 驱动——<input value={name} onChange={e => setName(e.target.value)}>,每次输入都更新 state,输入框显示永远等于 state。好处是数据尽在掌握:实时校验、禁用提交按钮、格式化输入(比如自动大写)都是一行判断的事。代价是每个控件一个 state,大表单会有不少样板代码。
非受控组件:值由 DOM 自己管,提交时用 ref 读出来。适合「只在提交时关心值」的简单场景(搜索框回车搜索),或集成第三方 DOM 库。文件上传必须用非受控(<input type="file"> 的 value 不能由 JS 设置),通过 ref 拿 e.target.files。
受控组件的几个细节。checkbox 用 checked 而不是 value;radio 组每个选项 checked={value === 'x'};select 把 value 放在 <select> 上而不是 option 的 selected。提交用 form 的 onSubmit + e.preventDefault() 阻止整页刷新。校验分两层:即时反馈(输入时提示格式)和提交校验(兜底),提交按钮的 disabled 由「表单是否有效」推导——注意这个「是否有效」是典型的可推导数据,不该单独存 state。
示例
实现一个受控表单的状态机:值、校验、提交按钮可用性全部从 state 推导(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 受控表单的核心:值存 state,校验是推导
function createForm() {
const state = { name: '', email: '', agreed: false };
return {
state,
setField(key, value) {
state[key] = value; // 真实组件里:setForm({ ...form, [key]: value })
},
// 校验规则:纯函数,从 state 推导
errors() {
const errs = {};
if (state.name.trim().length < 2) errs.name = '姓名至少 2 个字';
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(state.email)) errs.email = '邮箱格式不对';
if (!state.agreed) errs.agreed = '请勾选协议';
return errs;
},
canSubmit() {
return Object.keys(this.errors()).length === 0; // 可推导,不单独存
},
};
}
const form = createForm();
assert.strictEqual(form.canSubmit(), false); // 空表单不可提交
assert.deepStrictEqual(Object.keys(form.errors()), ['name', 'email', 'agreed']);
form.setField('name', '小明');
form.setField('email', 'not-an-email');
assert.strictEqual(form.errors().email, '邮箱格式不对');
form.setField('email', 'ming@example.com');
form.setField('agreed', true);
assert.strictEqual(form.canSubmit(), true); // 全部合法 → 可提交
console.log('填写后校验:', JSON.stringify(form.errors()));
console.log('canSubmit 由 errors 推导:', form.canSubmit());
真实组件:
import { useState } from 'react';
function SignupForm() {
const [form, setForm] = useState({ name: '', email: '', agreed: false });
const set = (key) => (e) =>
setForm({ ...form, [key]: e.target.type === 'checkbox' ? e.target.checked : e.target.value });
const errors = {};
if (form.name.trim().length < 2) errors.name = '姓名至少 2 个字';
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(form.email)) errors.email = '邮箱格式不对';
function handleSubmit(e) {
e.preventDefault();
if (Object.keys(errors).length === 0 && form.agreed) {
console.log('提交', form);
}
}
return (
<form onSubmit={handleSubmit}>
<input value={form.name} onChange={set('name')} placeholder="姓名" />
{form.name && errors.name && <em>{errors.name}</em>}
<input value={form.email} onChange={set('email')} placeholder="邮箱" />
<label>
<input type="checkbox" checked={form.agreed} onChange={set('agreed')} />
同意协议
</label>
<button disabled={Object.keys(errors).length > 0 || !form.agreed}>注册</button>
</form>
);
}
常见坑
- input 只给 value 不给 onChange:变成只读输入框,React 会警告;要么补 onChange,要么用 readOnly/defaultValue。
- checkbox 用 value 绑定:受控 checkbox 绑 checked,值在 checked 里。
- 每个控件手写一套 handler:用计算属性名
set(key)工厂收敛,或上表单库(react-hook-form)。 - 校验结果存 state:校验是 state 的推导,存 state 会导致「值改了错误没更新」的时序 bug。
- 大表单全受控导致卡顿:几百个控件每次都重渲染,考虑按字段拆分组件或非受控 + react-hook-form。
小结
受控 = value + onChange 驱动 state;校验与可提交性都从 state 推导;文件上传用非受控。下一章讲处理外部世界的钩子:useEffect。