一句话定义
受控组件:表单元素的值由 React state 驱动(value={v} onChange={...});非受控组件:值留在 DOM 自己手里,React 只在需要时用 ref 去取。绝大多数场景选受控。
为什么重要
表单是前端应用中最密集的「状态 ↔ 输入」交互,两种路线的选择影响校验、联动、可测试性。同时这对概念是理解 React 数据流「单一事实来源(Single Source of Truth)」的最佳教具,也适用于 checkbox、select、自定义输入组件。
前置知识
核心概念
- 单一事实来源:受控组件里,输入框显示什么由 state 决定,输入只是「提议」,state 接受后才生效。
- defaultValue:非受控的初始值,之后 DOM 自治。
- file input:天生非受控(React 无法替你持有文件对象),用 ref 读取。
- 即时校验 / 禁用提交 / 格式化输入:需要值参与渲染 → 受控。
- 提交时才需要值:非受控更省事。
原理 / 机制
同一需求的两种实现:
jsx// 受控:value 在 state,UI 是状态的投影
function Controlled() {
const [text, setText] = useState('');
return (
<input value={text} onChange={e => setText(e.target.value)} />
// text 可用于:长度提示、实时校验、禁用按钮、防抖搜索
);
}
// 非受控:值在 DOM,ref 取用
function Uncontrolled() {
const inputRef = useRef(null);
return (
<>
<input defaultValue="" ref={inputRef} />
<button onClick={() => console.log(inputRef.current.value)}>读取</button>
</>
);
}
受控的关键能力演示——「值不总是采纳」:
jsx// 只允许数字:state 过滤后才写回,输入框永远不会出现字母
function NumInput() {
const [v, setV] = useState('');
function onChange(e) {
if (/^\d*$/.test(e.target.value)) setV(e.target.value);
}
return <input value={v} onChange={onChange} />;
}
这在非受控下几乎无法实现——因为显示由 DOM 决定,React 无权否决用户敲的字符。
全表单的受控汇总与提交:
jsxconst [form, setForm] = useState({ name: '', email: '' });
const valid = form.name.length > 0 && form.email.includes('@');
<form onSubmit={e => { e.preventDefault(); if (valid) submit(form); }}>
<input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
<input value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} />
<button disabled={!valid}>提交</button>
</form>
直观类比
受控像报账制:你每花一笔钱(敲一个字)都要先报销进公司账本(state),界面显示的是账本余额;审计(校验/回显/测试)随查随有。非受控像零用金:钱在兜里,花起来快(少一次 state 更新),但月底要对账(提交时用 ref 捞值),中间过程不可见。
实例 / 案例
选择两种路线的真实场景对照:
| 场景 | 推荐 | 理由 |
|---|---|---|
| 登录/注册表单 | 受控 | 实时校验、错误提示、按钮禁用 |
| 搜索框 + 防抖 | 受控 | query 是后续请求的输入 |
| 一万行的配置导入界面 | 受控 + 库 | 需要整体校验/联动(或表单库) |
| 「立即评价」的纯文本域,提交即走 | 非受控 | 值只在提交时需要 |
| 文件上传 | 非受控 | file 对象不可由 React 持有 |
| 第三方组件嵌入(如日期选择器) | 视其 API | 好的组件库两者都提供 |
大型表单不要手搓受控状态——用 react-hook-form(内部以非受控为主、性能好)或 Formik(全受控)——见 kp-021 生态说明。
常见误区
- 受控输入忘绑 onChange:
<input value={text} />固定死值,敲字无效(React 还会警告)。 - 受控与非受控来回切换:
value={v ?? undefined}这种条件导致 React 警告且行为混乱;一个元素选定一种模式。 - 每次 onChange 里 setState 导致「输入卡顿」:其实是重渲染成本高,先查渲染链(kp-019),而不是怀疑受控模式。
- 以为受控 = 性能差:受控多一次渲染是事实,但输入框重渲染成本极低;真正的开销通常在列表/图表层。
自测题
- 用「单一事实来源」解释为什么受控输入可以实现「过滤非法字符」。
- 什么情况下必须非受控?举两个例子并说明受控为什么做不到。
- 把上面的受控表单改成非受控版本,指出你失去了哪些能力。
value={text}且没有 onChange,输入时发生什么?控制台说什么?
与其他知识点的关系
- 向前:是 kp-004「状态提升」在表单域的标准应用。
- 向后:kp-017 的 useRef 是非受控的钥匙;kp-025 的 React 19 Actions 提供了「表单提交」的新范式(Server Action / useActionState),会改变一部分受控表单的写法;kp-021 提到表单库选型。
延伸阅读
- Controlled and uncontrolled components(官方 FAQ 视角):<https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components>
- react-hook-form(生态参考):<https://react-hook-form.com/>