React 受控组件 & 非受控组件
一、核心前置概念:React 的「单向数据流」
React 是单向数据流的框架:数据(state/props)的流向是「自上而下」的,只能从父组件传递给子组件,数据变更只会触发 UI 重新渲染,表单元素本身的原生数据(比如 input 的 value)默认和 React 的数据是隔离的。 这是理解受控/非受控组件的核心前提,两种组件的本质,就是「React 要不要接管表单元素的原生数据控制权」。
二、一、受控组件(Controlled Component)
定义
React 组件通过 state(或父组件的 props)完全接管表单元素的「值」和「值更新」,表单元素自身不再维护原生的内部状态,它的任何值变化都由 React 来驱动和管理。
核心特征
- 表单元素的
value属性 被 React 的state/props绑定(赋值),元素展示的值 = React 数据的值; - 表单元素绑定
onChange事件(值变更回调),当表单值发生变化时,立刻通过setState更新 React 的state; - 表单元素的「值」完全受 React 控制,表单的一切值变更行为,都必须经过 React 的数据更新流程。
核心工作流程
用户输入 → 触发表单元素 onChange 事件 → 调用 setState 更新 React 中的状态 → 状态更新触发组件重新渲染 → 表单元素的 value 被重新赋值为最新状态 → 页面展示最新值。
完整可运行示例(最常用的 input 受控组件)
import { useState } from 'react';
// 受控组件示例
const ControlledInput = () => {
// 1. 用 state 存储表单值,作为唯一数据源
const [inputValue, setInputValue] = useState('');
// 2. 绑定 onChange 事件,监听值变化并更新 state
const handleChange = (e) => {
setInputValue(e.target.value);
};
// 3. 绑定表单提交事件,获取的是 state 中的值(可靠)
const handleSubmit = (e) => {
e.preventDefault();
console.log('提交的内容:', inputValue);
};
return (
<form onSubmit={handleSubmit}>
{/* 核心:value 绑定 state,onChange 绑定更新函数 */}
<input
type="text"
value={inputValue}
onChange={handleChange}
placeholder="受控输入框"
/>
<button type="submit">提交</button>
</form>
);
};
export default ControlledInput;
三、二、非受控组件(Uncontrolled Component)
定义
表单元素的「值」由自身的原生 DOM 节点维护,React 不接管它的状态,组件渲染时,表单元素用自己的默认值展示,后续的输入变化也不会同步到 React 的 state 中。
核心特征
- 表单元素不绑定
value属性,而是用defaultValue属性 设置「初始默认值」(只在组件首次渲染时生效,后续不会被 React 更新); - 不绑定
onChange事件,不需要实时更新 React 的state; - React 想要获取表单的值时,需要通过
ref手动获取原生 DOM 节点,从 DOM 上读取当前值; - 表单的「值」不受 React 控制,由浏览器原生 DOM 管理,是「非受控」的核心体现。
完整可运行示例(最常用的 input 非受控组件)
import { useRef, useEffect } from 'react';
const UncontrolledInput = () => {
const inputRef = useRef(null);
useEffect(() => {
// 模拟获取数据
setTimeout(() => {
// 直接操作 DOM 赋值(唯一有效方式)
inputRef.current.value = '我是初始内容';
}, 1000);
}, []);
const handleSubmit = (e) => {
e.preventDefault();
console.log('提交:', inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} />
<button type="submit">提交</button>
</form>
);
};
export default UncontrolledInput;
四、关键注意点
1. 不要给非受控组件绑定 value 属性!
如果给非受控组件(用 defaultValue+ref 的表单)绑定了 value,React 会报一个明确的警告:
Warning: You provided a
valueprop to a form field without anonChangehandler. This will render a read-only field.
原因:绑定 value 意味着「React 要控制值」,但又没有 onChange 更新值,最终会导致表单元素只读,用户无法输入任何内容,这是新手最常见的错误!
2. 补充:复选框/单选框的特殊属性
- 普通输入框/文本域:受控用
value+onChange,非受控用defaultValue+ref - 复选框(
checkbox)/单选框(radio):受控用checked+onChange,非受控用defaultChecked+ref本质逻辑不变,只是属性名不同,都是「受控绑定『当前值』,非受控绑定『默认值』」。
五、 受控 vs 非受控 核心区别
这是最核心的考点,建议熟记,表格中把所有关键差异都整理好了:
| 对比维度 | 受控组件 (Controlled) | 非受控组件 (Uncontrolled) |
|---|---|---|
| 数据控制权 | 由 React 的 state/props 完全控制 | 由 DOM 原生节点自身维护 |
| 表单值绑定 | 绑定 value / checked 属性 | 绑定 defaultValue / defaultChecked 属性 |
| 值更新方式 | 必须绑定 onChange 事件,实时更新 state | 无需绑定 onChange,无实时数据更新 |
| 值获取方式 | 直接从 React 的 state 中读取(可靠) | 通过 ref 获取原生 DOM 节点的 value |
| 数据一致性 | 表单值 ≡ state 值,始终一致 | 表单值是 DOM 原生值,和 React 数据无关联 |
| 实时校验能力 | 支持(输入时实时触发 onChange,可做校验) | 不支持(只能在提交时获取值后校验) |
| 灵活性 | 高(可实时格式化、禁用、联动表单) | 低(只能做简单的表单提交) |
六、选型原则:什么时候用受控?什么时候用非受控?
优先用「受控组件」的场景(90% 的业务场景)
这是 React 官方和行业的主流推荐方案,满足绝大多数业务需求,推荐优先使用:
- 需要实时处理用户输入:比如实时表单校验(输入时提示手机号格式错误)、实时格式化(输入金额自动加千分位)、输入防抖搜索;
- 需要表单联动:比如「选择省份后,联动更新城市下拉框」、「勾选同意协议后,才能点击提交按钮」;
- 需要禁用/重置表单:比如根据状态禁用输入框、点击「重置」按钮一键清空表单值(直接更新 state 即可);
- 需要精准控制表单值:比如限制输入长度、只能输入数字、密码明文/密文切换;
- 复杂表单场景:多表单项联动、分步表单、表单提交前需要做数据预处理。
适合用「非受控组件」的场景(10% 的简单场景)
非受控组件不是「不好」,而是更适合简单场景,代码更简洁,无需维护多余的 state 和 onChange,是「够用就好」的选择:
- 简单的表单提交:只需要在用户点击提交时获取一次值,不需要实时处理输入(比如登录页的用户名密码、简单的留言板);
- 文件上传控件 (
<input type="file" />):✅ 这是唯一必须用非受控组件的场景!因为文件上传控件的value是只读的,浏览器不允许 JS 手动修改文件路径,只能通过ref获取原生 DOM 的files属性,没有任何受控的写法; - 不需要实时校验/格式化的表单:比如简单的备注输入框、昵称输入框,只需要提交时拿到值即可;
- 快速开发的简单场景:追求代码简洁,不想写多余的 state 和事件处理函数。
总结
- 受控组件:React 管值,
value/checked+onChange+state,数据和视图强绑定,功能强大,是主流方案; - 非受控组件:DOM 管值,
defaultValue/defaultChecked+ref,无需维护状态,代码简洁,适合简单场景; - 核心区别:谁掌握表单值的「控制权」 —— React 控制就是受控,DOM 控制就是非受控;
- 避坑核心:非受控组件不能绑
value,文件上传只能用非受控; - 选型口诀:复杂场景用受控,简单场景用非受控,文件上传必用非受控。