写在前面:今天学了一个 React 表单的核心概念------受控组件(Controlled)和非受控组件(Uncontrolled) 。一开始我完全懵了:同样是输入框,怎么还分"受控""非受控"?老师说:"受控组件是 React 管着输入框的值,非受控组件是输入框自己管自己的值------React 需要用的时候才去'抢'过来。" 听完我想到一个比喻:受控 = 房东管着房租(状态),非受控 = 租客自己管钱,房东要用钱时才去问。 一整个下午,我们把两种组件都写了一遍,还做了一个注册表单,终于搞明白了。
一、什么是"受控"?先看两种写法
1.1 受控组件(ControlledInput)
看 ControlledInput.jsx:
jsx
import { useState } from 'react';
function ControlledInput() {
const [value, setValue] = useState('');
return (
<>
ControlledInput
<input
type="text"
value={value} // ← 值是 React 状态控制的
onChange={(e) => {
setValue(e.target.value); // ← 输入时更新状态
}}
/>
</>
);
}
受控组件的特征:value + onChange 成对出现。
value={value}:输入框显示的值,来自 React 的 state。onChange:用户输入时,把新值写回 state。
数据流:
scss
用户输入
↓ onChange
setValue(e.target.value) // 更新 React 状态
↓ 重新渲染
<input value={value}> // React 把新值"塞回"输入框
状态(React)和界面(输入框)永远一致------输入框的值完全由 React 控制。
1.2 非受控组件(UncontrolledInput)
看 UncontrolledInput.jsx:
jsx
import { useRef } from 'react';
function UncontrolledInput() {
const inputRef = useRef(null);
function handleClick() {
console.log(inputRef.current.value); // 需要时再"抢"值
}
return (
<>
UncontrolledInput
<input type="text" ref={inputRef} />
<button onClick={handleClick}>获取输入值</button>
</>
);
}
非受控组件的特征:用 ref 获取值,没有 value 和 onChange。
- 输入框的值由浏览器 DOM 自己管理。
- React 不管它的值是什么。
- 需要时通过
inputRef.current.value从 DOM 里"抢"出来。
数据流:
arduino
用户输入
↓ 浏览器自己处理
输入框的值存在 DOM 里(React 不知道)
↓ 点击按钮
inputRef.current.value // 从 DOM 里读取
二、"房东和租客"比喻
2.1 受控组件 = 房东管房租
perl
受控组件:
房租(输入值)由房东(React state)管理
↓
租客(用户)交钱 → 房东记账(setState)→ 房东统一管理
↓
每次要查账,直接看房东的账本(state),永远准确
好处:React 随时知道输入框的值(状态即真相)。
2.2 非受控组件 = 租客自己管钱
非受控组件:
钱(输入值)由租客(DOM)自己管
↓
房东(React)不知道租客有多少钱
↓
房东要用钱时,才去问租客(inputRef.current.value)
好处:代码简单,不需要每次输入都更新状态;坏处:React 不知道当前值。
2.3 对比表
| 对比 | 受控组件 | 非受控组件 |
|---|---|---|
| 值的来源 | React state | DOM 自己 |
| 获取值 | 直接读 state | ref.current.value |
| 每次输入 | 触发 setState(重新渲染) | 不触发渲染 |
| 代码量 | 多(value + onChange) | 少(ref 一行) |
| 实时校验 | ✅ 可以(状态即值) | ❌ 不方便 |
| 适用场景 | 表单校验、联动、复杂表单 | 简单输入、评论框 |
三、实战:评论框(非受控)
看 CommentBox.jsx:
jsx
import { useRef } from 'react';
function CommentBox() {
const textareaRef = useRef(null);
function handleSubmit() {
const comment = textareaRef.current.value;
if (!comment) return;
console.log(comment); // 拿到评论内容
}
return (
<>
<textarea
placeholder="输入评论..."
ref={textareaRef}
/>
<button onClick={handleSubmit}>提交评论</button>
</>
);
}
评论框为什么适合非受控?
- 用户输入评论时,不需要实时校验。
- 只有点击"提交"时才需要值。
- 用
ref在提交时读取,避免了每次输入都 setState 的性能浪费。
老师说:
"非受控------用
useRef获取值。简单输入场景,不需要实时响应。"
四、实战:注册表单(受控)
看 RegisterForm.jsx:
jsx
import { useState } from 'react';
function RegisterForm() {
// 用一个对象管理整个表单
const [form, setForm] = useState({
username: '',
password: '',
});
// 通用 onChange:用 name 属性动态更新对应的字段
const handleChange = (e) => {
setForm({
...form,
[e.target.name]: e.target.value, // 计算属性名!
});
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(form); // 直接拿到完整表单数据
};
return (
<div>
<input
name='username'
value={form.username}
onChange={handleChange}
placeholder="请输入用户名"
type="text"
/>
<input
name='password'
value={form.password}
onChange={handleChange}
placeholder="请输入密码"
type="password"
/>
<button type="submit" onClick={handleSubmit}>提交</button>
</div>
);
}
4.1 为什么要用受控表单?
注册表单的痛点:提交时需要校验用户名是否为空、密码长度是否够。
非受控表单:提交时去 DOM 一个个读值,还要手动校验,麻烦。
受控表单:
arduino
用户输入
↓ onChange
[e.target.name]: e.target.value // 自动更新到 form 对象
↓
form 就是"实时真相"------React 随时知道所有字段的值
↓ 提交时
console.log(form) // 直接拿到 { username, password }
4.2 计算属性名的妙用
jsx
[e.target.name]: e.target.value
这是 ES6 的"计算属性名"语法:
- 输入框
name="username"→ 更新form.username。 - 输入框
name="password"→ 更新form.password。
一个 handleChange 通吃所有字段------不用为每个字段写一个 onChange!
4.3 不可变更新
jsx
setForm({
...form, // 展开旧值(保留其他字段)
[e.target.name]: e.target.value, // 只更新当前字段
});
...form 展开运算符保留其他字段,只改当前输入框对应的字段------这是 React 不可变数据更新的标准写法。
五、什么时候用哪种?
5.1 决策指南
| 场景 | 选哪种 | 为什么 |
|---|---|---|
| 注册/登录表单(要校验) | 受控 | 状态即真相,方便实时校验 |
| 搜索框(要联动) | 受控 | 输入即触发搜索 |
| 评论框(提交才用值) | 非受控 | 简单,省性能 |
| 文件上传(value 只读) | 非受控 | 受控组件管不了 file 输入 |
| 富文本编辑器 | 非受控 | 用 ref 获取内容 |
5.2 老师的话
老师说:
"受控组件------响应式状态控制 input,收集用户的输入。非受控------用
useRef,输入框自己管值。"
一句话:需要 React 实时知道值(校验、联动)→ 受控;只是最后取一次值 → 非受控。
六、总结
| 概念 | 说明 |
|---|---|
| 受控组件 | value + onChange,值由 React state 控制 |
| 非受控组件 | ref 获取值,值由 DOM 自己管理 |
| 受控数据流 | 输入 → setState → 重新渲染 → 塞回输入框 |
| 非受控数据流 | 输入 → DOM 存着 → 需要时 ref 读取 |
| 计算属性名 | [e.target.name]: value 动态更新对象字段 |
| 不可变更新 | {...form, [name]: value} 保留其他字段 |
| 选择标准 | 要实时校验联动 → 受控;只要最终值 → 非受控 |
受控组件和非受控组件没有绝对的好坏------受控适合需要 React 实时掌握值的复杂表单,非受控适合只需要最终取值的简单场景。理解两者的区别,才能在合适的场景用对的方式。
写在最后
今天最大的收获,是终于分清了受控和非受控。以前写表单全靠"运气"------要么 value 没写 onChange 输入框变灰,要么用 ref 到处取值。现在明白了:受控组件用 state 管值,适合校验联动;非受控组件用 ref 取值,适合简单输入。 而且计算属性名 [e.target.name] 这个技巧太香了------一个 onChange 通吃整个表单。
下次面试官问你:"受控组件和非受控组件有什么区别?"
你可以淡定地说:
"受控组件和非受控组件的核心区别是'值由谁管理'。受控组件用 value + onChange,输入框的值由 React state 控制,用户输入触发 setState 更新状态,再通过 value 回显------React 实时知道输入框的值,适合需要校验、联动的场景(如注册表单)。非受控组件用 ref(如 useRef),输入框的值由浏览器 DOM 自己管理,需要时通过 ref.current.value 读取,React 不参与值的实时管理------适合只需最终取值的场景(如评论框)。选择标准:需要实时知道值 → 受控;只要最终值 → 非受控。受控表单常用 [e.target.name]: value 计算属性名 + ...form 展开运算符实现通用 onChange。"
然后看着面试官满意的表情,心里默念:这波,又稳了。
本文所有代码示例均来自课堂学习资料,真实可运行。