React 受控组件和非受控组件有什么区别?
简化版
受控组件的表单值由 React state 控制,输入变化通过 onChange 更新 state;非受控组件的值由 DOM 自己维护,React 通过 ref 在需要时读取。受控组件更可控,非受控组件更轻量。
详细版
受控:
const [value, setValue] = useState('');
<input value={value} onChange={e => setValue(e.target.value)} />
非受控:
const ref = useRef(null);
<input ref={ref} defaultValue="Tom" />
受控适合实时校验、联动、格式化、提交前状态统一管理。非受控适合简单表单、文件上传、第三方 DOM 插件或不需要实时同步的场景。
完整版教学
一、受控组件把 DOM 值交给 React
受控组件中,输入框显示什么由 state 决定。用户输入触发 onChange,React 更新 state,再重新渲染 input。
这种方式让数据流清晰:UI 是 state 的映射,表单值随时在 React 中可见。
二、非受控组件让 DOM 自己维护值
非受控组件使用 defaultValue 初始化,后续输入变化不每次同步到 React state。需要提交时,通过 ref 读取 DOM 当前值。
文件上传 <input type="file"> 通常就是非受控,因为文件值不能像普通文本一样由 React 设置。
function handleSubmit(e) {
e.preventDefault();
const data = new FormData(e.currentTarget);
console.log(data.get('email'));
}
非受控表单不等于必须逐个 ref 读取,原生 FormData 可以按 name 一次取得提交快照。代价是提交前 React 不天然拥有每个字段的实时值,跨字段联动仍需监听或引入表单状态层。
重置时也要分清契约:受控字段重置 state,非受控字段可调用表单 reset 或重新挂载,但不能混用两套来源竞争同一个当前值。
三、两者的取舍
受控组件更适合复杂表单:字段联动、实时校验、按钮禁用、格式化输入、错误提示。缺点是每次输入都触发状态更新,超大表单需要优化。
非受控组件更简单,减少 React 状态更新,但状态不集中,实时联动较麻烦。
四、面试追问与工程落地
常见追问是“为什么 input 从 undefined 变成字符串会警告”。这是 React 检测到组件从非受控变成受控。应该给受控 input 明确初始值,比如空字符串,而不是一开始 undefined。
工程里表单库也在做权衡。Formik 偏受控思路,React Hook Form 大量利用非受控和 ref 降低重渲染。选择不是绝对,要看表单复杂度和性能要求。
五、控制权由 value/checked 契约决定
文本输入传 value 后就是受控组件,复选框和单选框则由 checked 控制;对应的 onChange 必须同步把 DOM 提供的新值写回 state。若只传 value 却不更新它,React 每次提交都会把输入框恢复成旧值,用户表现上就像无法输入。
<input value={name} onChange={e => setName(e.target.value)} />
<input type="checkbox" checked={agreed}
onChange={e => setAgreed(e.target.checked)} />
defaultValue 和 defaultChecked 只定义初始值,挂载后修改这些 props 不负责覆盖用户已经输入的当前值。文件选择框的文件列表由浏览器和用户操作控制,通常通过 ref 或表单提交读取,不能像文本值一样用 React state 指定任意本地文件路径。
| 维度 | 受控组件 | 非受控组件 |
|---|---|---|
| 当前值来源 | React state/props | DOM 内部状态 |
| 初始化 | value / checked | defaultValue / defaultChecked |
| 读取时机 | render 中随时可用 | 提交时用 ref/FormData |
| 实时校验与联动 | 直接 | 需要额外监听或提交后处理 |
| 每次输入的 React 更新 | 通常有 | 可以没有 |
六、用更新范围衡量大型表单成本
假设表单有 200 个字段,每敲一个字符都把整个表单对象放到顶层 state;若所有字段组件都随父级执行,一次按键可能带来约 200 个字段的 render 检查。把字段状态下沉、按字段订阅,或采用以非受控注册为主的表单库,可以把更新范围缩小,但要承担校验、默认值和提交数据同步的设计成本。
受控不等于一定慢,关键是一次状态更新波及多少组件以及每个组件渲染多重。应使用 Profiler 测量输入延迟,再决定拆组件、延迟昂贵派生计算或改用非受控;不要为了“少一次 render”牺牲必需的即时业务约束。
选择锚点:需要每个时刻都由 React 决定显示值,就受控;只需要初始化并在提交时读取,就可以非受控。
七、常见误区与追问
- 误区:传了 onChange 的输入框就是受控组件。 是否受控取决于是否传入
value或checked,onChange 本身不能定义控制权。 - 误区:修改 defaultValue 会持续覆盖输入框。 它只提供初始值,挂载后的当前值仍由 DOM 维护。
- 误区:非受控组件完全不受 React 管理。 React 仍创建和卸载节点,只是表单当前值不由 React state 驱动。
- 追问:为什么 value 从 undefined 变成字符串会警告? 组件生命周期中从非受控切到受控会造成契约不一致,文本值应从
''开始。 - 追问:checkbox 为什么读取 checked 而不是 value? value 是提交值,勾选状态由布尔型 checked 表示。
- 追问:文件输入为什么通常非受控? 浏览器出于安全模型只允许用户选择文件,应用不能用普通 value prop 指定本地文件。
- 追问:大型表单一定要改成非受控吗? 不一定,应先测更新范围;字段拆分、局部状态和延迟计算也能保留受控模型。
八、加强记忆
受控组件是“值在 React state”,非受控组件是“值在 DOM”。要实时联动和校验用受控,要轻量和少渲染可考虑非受控。
同时记住文本看 value、勾选看 checked,控制方式在组件生命周期内不能来回切换。