1、React表单基础:受控组件与非受控组件的概念与区别
表单处理,是每个React开发者都绕不开的话题。我记得刚接触React时,就被「受控」和「非受控」这两个词搞得有点懵。说白了,它们就是两种管理表单数据的方式——一个交给React管,一个交给DOM自己管。
今天我们就来彻底搞清楚这两个概念,以及在实际项目中该怎么选。
1.1 受控组件:数据交给React
受控组件,就是表单的值由React state来驱动。你想想看,输入框里显示什么,不取决于用户敲了什么,而取决于state里存了什么。
function ControlledInput() {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
};
return (
<input
type="text"
value={value}
onChange={handleChange}
/>
);
}
这里有个关键点:value 属性绑定了 state,onChange 负责更新 state。用户每次敲键盘,流程都是:输入框触发 onChange → setValue 更新 state → 组件重新渲染 → 输入框显示新值。
核心特征:React state 是「唯一数据源」。表单的值完全由 React 控制。
我在项目中遇到过这样一个场景:需要实时校验用户输入的邮箱格式。用受控组件就特别方便——每次 onChange 时都能拿到最新值,立刻做校验,然后决定是否显示错误提示。
1.2 非受控组件:交给DOM自己管
非受控组件就简单多了。表单的值由 DOM 自己维护,React 不插手。你需要取值时,通过 ref 去 DOM 里拿。
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = () => {
console.log('当前输入值:', inputRef.current.value);
};
return (
<div>
<input type="text" defaultValue="默认值" ref={inputRef} />
<button onClick={handleSubmit}>提交</button>
</div>
);
}
注意这里用的是 defaultValue 而不是 value。defaultValue 只在初次渲染时生效,之后用户怎么改,React 都不管。
小技巧:非受控组件适合「一次性取值」的场景,比如表单提交时统一读取所有字段。
1.3 两者的核心区别
我习惯用一张表来对比,这样更直观:
| 对比维度 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据源 | React state | DOM 自身 |
| 取值方式 | 直接从 state 读取 | 通过 ref 读取 DOM |
| 初始值设置 | state 初始值 | defaultValue / defaultChecked |
| 实时校验 | 天然支持 | 需要额外监听事件 |
| 代码量 | 稍多 | 较少 |
| 性能 | 每次输入都触发渲染 | 不触发渲染 |
为什么会这样?说白了,受控组件每次 state 变化都会触发重新渲染,而非受控组件只在你主动通过 ref 取值时才和 React 交互。
1.4 如何选择合适的场景
这个问题,我个人的经验是:优先用受控组件,除非有明确的理由用非受控。
具体来说:
- 需要实时校验的场景——比如密码强度检测、邮箱格式验证,用受控组件。我在项目中做过一个注册表单,密码输入时要实时显示强度条,非受控根本没法做。
- 需要联动其他字段——比如选择国家后自动填充区号,受控组件让联动变得很简单。
- 简单的、一次性的表单——比如搜索框、文件上传,用非受控就够了。你想想看,搜索框用户输完直接提交,中间不需要任何校验,何必多写一堆 state 呢?
- 性能敏感的大表单——如果表单有上百个输入框,每个都受控会导致频繁渲染。这时候可以考虑用非受控,或者配合 useRef 做局部控制。
避坑指南:我曾经在一个项目中,把文件上传 input 写成了受控组件,结果发现 value 属性无法被程序设置(浏览器安全限制),导致上传功能直接失效。嗯,文件上传这种场景,老老实实用非受控吧。
1.5 混合使用也是可以的
别以为一个表单里只能选一种。我经常在一个表单里混用:大部分字段用受控,少数简单的字段用非受控。
function MixedForm() {
const [email, setEmail] = useState('');
const fileRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const file = fileRef.current.files[0];
console.log('邮箱:', email, '文件:', file.name);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input type="file" ref={fileRef} />
<button type="submit">提交</button>
</form>
);
}
这样既享受了受控组件的实时校验能力,又避免了文件上传的坑。
1.6 小结
受控组件和非受控组件,没有绝对的好坏。我的建议是:默认选受控,简单场景选非受控,复杂场景可以混用。你只要记住一点——受控组件让 React 掌控数据流,非受控组件把控制权还给 DOM。理解了这一点,你就知道该怎么选了。
下一章,我们会深入表单校验的实战技巧,包括如何封装可复用的校验规则、如何处理异步校验等。到时候我会分享一些我在大厂项目中踩过的坑,敬请期待。