表单是 Web 应用中最常见的交互形式,React 处理表单的方式与原生 HTML 存在关键差异。理解受控组件与非受控组件的区别,是掌握 React 表单开发的基础。
受控组件(Controlled Components)
受控组件是指表单元素的值由 React 的 state 统一管理,并通过事件处理函数更新 state。React 成为状态的“唯一数据源”,表单输入、状态、视图三者保持严格同步。
function ControlledForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log('提交数据:', { name, email });
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="姓名"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
/>
<button type="submit">提交</button>
</form>
);
}
工作流程:用户输入 → 触发 onChange → 更新 state → React 重新渲染组件,value 属性同步最新值。
受控组件的优势:
- 数据实时可用:state 始终持有最新值,无需手动读取 DOM。
- 灵活控制输入:可以在
onChange中格式化数据(如自动去除空格、限制字符数)。 - 表单验证便捷:实时校验输入并动态显示错误信息。
- 条件渲染依赖:表单状态可直接驱动其他 UI 变化。
常见模式:实时格式化输入
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value.toUpperCase())}
/>
用户输入自动转为大写,一切在 React 控制之中。
非受控组件(Uncontrolled Components)
非受控组件是指表单元素的值由 DOM 自身维护,React 不直接管理其状态。需要时通过 ref 从 DOM 节点读取当前值,而非通过 state 驱动。
function UncontrolledForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('提交数据:', {
name: nameRef.current.value,
email: emailRef.current.value,
});
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={nameRef} placeholder="姓名" defaultValue="张三" />
<input type="email" ref={emailRef} placeholder="邮箱" />
<button type="submit">提交</button>
</form>
);
}
关键点:
- 使用
ref代替value+onChange。 - 初始值通过
defaultValue(或defaultChecked)设置,而不是value。 - 数据只在需要时(如提交表单)读取,不会每次输入都触发渲染。
非受控组件的适用场景:
- 简单表单,不需要实时校验或格式化。
- 需要集成非 React 代码(如第三方 DOM 库)。
- 性能敏感场景,避免频繁重渲染(React 18 的自动批处理已缓解大部分情况)。
受控 vs 非受控:如何选择
| 特性 | 受控组件 | 非受控组件 |
|------|----------|------------|
| 数据源 | React state | DOM 节点 |
| 实时数据访问 | 是,state 始终最新 | 否,需要时读取 |
| 代码量 | 较多(value + onChange) | 较少(ref) |
| 表单验证 | 实时校验,易实现 | 提交时校验为主 |
| 性能 | 每次输入触发重渲染 | 仅在需要时读取,不触发额外渲染 |
| 推荐度 | 首选方案,React 官方推荐 | 特定场景或快速原型 |
实际建议:大多数情况下使用受控组件,因为它更符合 React 声明式的数据流理念,且提供了最大的控制力。当表单极其简单或需要优化高频输入性能时(如富文本编辑器),可以非受控模式作为补充。
表单数据绑定与提交
React 没有 Vue 中的 v-model 双向绑定语法糖,而是通过 value(展示数据) + onChange(更新数据) 实现单向数据流。这种显式的方式让数据流向更清晰,缺点是需要在每个字段上写重复代码。
封装通用的表单绑定 Hook 减少重复:
function useFormField(initialValue = '') {
const [value, setValue] = useState(initialValue);
const onChange = (e) => setValue(e.target.value);
return { value, onChange };
}
// 使用
function MyForm() {
const nameField = useFormField('');
const emailField = useFormField('');
return (
<form>
<input type="text" {...nameField} placeholder="姓名" />
<input type="email" {...emailField} placeholder="邮箱" />
</form>
);
}
处理多种表单元素:
- 文本框/密码/邮件:
value+onChange。 - 复选框:
checked属性,onChange中读取e.target.checked。 - 单选按钮:相同
name属性,checked根据当前值判断。 - 下拉选择:
<select>的value属性(不同于原生 HTML 的selected属性)。
function FullForm() {
const [form, setForm] = useState({
subscribe: false,
gender: 'male',
city: 'beijing',
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setForm({
...form,
[name]: type === 'checkbox' ? checked : value,
});
};
return (
<form>
<label>
<input
type="checkbox"
name="subscribe"
checked={form.subscribe}
onChange={handleChange}
/>
订阅邮件
</label>
<div>
<label>
<input
type="radio"
name="gender"
value="male"
checked={form.gender === 'male'}
onChange={handleChange}
/>
男
</label>
<label>
<input
type="radio"
name="gender"
value="female"
checked={form.gender === 'female'}
onChange={handleChange}
/>
女
</label>
</div>
<select name="city" value={form.city} onChange={handleChange}>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
</select>
</form>
);
}
实战注意事项
- 受控组件的
value必须绑定 state:如果设置了value而没有提供onChange,React 会警告,因为此时输入框变成了只读。若确实需要只读,使用readOnly属性。
- 表单提交时的默认行为:
onSubmit事件中必须调用e.preventDefault()阻止页面刷新。
- 非受控组件的
defaultValue只在挂载时生效:后续更新不会改变 DOM 值。如需动态重置表单,最好使用受控组件,或结合key属性强制重新挂载。
- 大型表单推荐使用专业库:React Hook Form(利用非受控模式,性能优异)或 Formik(声明式校验)能显著减少样板代码,并提供完善的校验和错误处理。但掌握原生控制原理是优雅使用这些库的前提。
受控组件体现了 React 声明式编程的精髓:描述状态到视图的映射,让框架保证它们的一致性。掌握后,你将能够从容应对从简单登录框到复杂多步骤表单的任何场景。