在 React 中处理表单元素时,你需要面对一个关键的设计决策:使用受控组件还是非受控组件。这两种模式决定了表单数据的状态由谁来管理,也直接影响代码的复杂度、性能和可维护性。
概念对比
| 特性 | 受控组件 | 非受控组件 |
|------|----------|------------|
| 数据存储 | 状态完全由 React state 控制 | 数据存放在真实 DOM 中,React 不直接管理 |
| 数据同步 | 每次输入都会更新 state 并触发重渲染 | 仅在需要时(如提交)通过 ref 读取 DOM 值 |
| 实现方式 | 设置 value/checked + onChange 处理 | 使用 defaultValue/defaultChecked + ref |
| 性能特征 | 每次按键都会重渲染,但数据与 UI 高度一致 | 无额外渲染开销,数据取值需手动读取 |
| 适用场景 | 需要实时校验、格式化、动态控制的表单 | 简单表单、大型表单性能优先、非 React 环境集成 |
受控组件:React 掌管一切
在受控组件中,表单元素的 value(或 checked)属性被绑定到组件的某个 state,并通过 onChange 事件即时更新该 state。React 成为所有输入数据的唯一数据源(Single Source of Truth)。
function ControlledInput() {
const [name, setName] = useState('');
const handleChange = (e) => {
setName(e.target.value);
};
return (
<div>
<input
type="text"
value={name}
onChange={handleChange}
placeholder="输入姓名"
/>
<p>当前输入:{name}</p>
</div>
);
}
受控组件的优势在于数据与 UI 的绝对同步。你可以方便地在 handleChange 中添加实时校验、格式化、限制输入等逻辑:
const handleChange = (e) => {
// 只允许字母
const onlyLetters = e.target.value.replace(/[^a-zA-Z]/g, '');
setName(onlyLetters);
};
对于复选框、单选按钮、下拉选择等,同样遵循受控模式,只是使用的属性不同:
// 下拉选择
<select value={city} onChange={(e) => setCity(e.target.value)}>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
</select>
// 复选框
<input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} />
受控组件是 React 官方推荐的主流写法,尤其适合表单与 UI 其他部分紧密联动的场景。
非受控组件:放手让 DOM 自理
非受控组件将表单数据存储于真实 DOM 节点中,React 仅在需要时通过 ref 读取值。它的行为更接近传统的 HTML 表单。
function UncontrolledForm() {
const nameRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
alert('提交的姓名:' + nameRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={nameRef} defaultValue="默认名字" />
<button type="submit">提交</button>
</form>
);
}
注意:初始化时可以使用 defaultValue 设置默认值,但后续的更新不会触发 React 重渲染。读取值时直接访问 ref.current.value。
文件上传是一个典型的非受控场景,因为文件对象无法通过 value 属性直接绑定:
function FileUpload() {
const fileRef = useRef(null);
const handleUpload = () => {
const file = fileRef.current.files[0];
console.log('选中的文件:', file);
};
return (
<div>
<input type="file" ref={fileRef} />
<button onClick={handleUpload}>上传</button>
</div>
);
}
选型指南:何时用哪种?
优先选择受控组件的场景:
- 需要实时表单校验(如输入格式检查、密码强度提示)。
- 需要动态控制 UI(如根据输入内容禁用/启用提交按钮、联动下拉选项)。
- 需要格式化输入值(如手机号自动加空格、金额千分位分隔)。
- 需要重置表单到初始状态时,直接重置 state 即可。
优先选择非受控组件的场景:
- 大型表单,包含几十个字段且不需要即时反馈,减少大量 onChange 造成的重渲染。
- 集成第三方非 React 库(如一些旧式 jQuery 插件),需要直接操作 DOM。
- 文件输入等无法用 value 表达的数据类型。
- 表单提交时才关心数据,如简单的搜索框、登录表单(可接受触发一次提交获取值)。
实际项目中的混合使用:
许多开发者倾向于受控组件的一致性,但也可以结合 React Hook Form 等库实现非受控模式的高性能 + 受控式的便捷校验。React Hook Form 内部采用 ref 获取表单值,避免了每次输入都重渲染整个表单,同时提供了 watch、errors 等实时响应能力,是高性能表单的最佳实践之一。
import { useForm } from 'react-hook-form';
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('username', { required: true })}
placeholder="用户名"
/>
{errors.username && <span>用户名必填</span>}
<button type="submit">登录</button>
</form>
);
}
常见误区
- “受控组件必须绑定 onChange”:如果只设了
value而没有onChange,输入框将变成只读状态,用户无法输入。React 要求受控组件必须提供更改处理逻辑。 - 混用 defaultValue 和 value:React 中如果设置了
value属性,组件会进入受控状态,此时defaultValue不生效。只能二选一。 - 频繁更新的性能问题:受控组件每次按键都触发重渲染,虽然不是大问题,但在极端大型表单中可能造成卡顿。这时可考虑非受控或使用
React.memo/useCallback优化子组件。
总结
受控与非受控是 React 表单处理的两个基本范式。受控组件提供了更强的控制力和数据一致性,是大多数场景的默认选择;非受控组件则以更少的模板代码和性能优势在某些场景中占优。掌握两者的实现方式和适用边界,能让你在面对不同需求时做出最合理的选型。