受控组件让 React 当表单的”大脑”,非受控组件让 DOM 自己管自己——选谁取决于你需要多少控制权。
一、两种哲学:谁来管表单的值?
React 世界里,表单元素(<input>、<textarea>、<select>)有个独特的身份问题:它的值到底归谁管?
这引出两种模式:
- 受控组件:表单值由 React state 控制。
value由 state 决定,onChange更新 state。React 是唯一的”数据源”。 - 非受控组件:表单值由 DOM 自己管理。React 不干预,需要时用
ref读取。
生活类比:
- 受控组件像公司报账——每笔钱都走审批流程(state),你想花多少得先申请,账本上永远有最新记录。
- 非受控组件像个人钱包——你自己管钱(DOM),需要查余额时再打开看一眼(ref),平时没人管你。
二、核心对比:一张表看清差异
| 维度 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据源 | React state | DOM 自身 |
| 读取值 | 从 state 读 | 从 ref.current.value 读 |
| 实时验证 | ✅ 每次 onChange 都能校验 | ❌ 不方便,需额外监听 |
| 条件禁用提交 | ✅ state 驱动,轻松控制 | ❌ 需手动检查 ref |
| 初始值 | value={state} | defaultValue="..." |
| 代码量 | 稍多(state + onChange) | 较少(ref 即可) |
| 适用场景 | 需要实时反馈、复杂校验 | 文件上传、简单表单、集成第三方 |
选择的核心逻辑:你需要对表单值做多少事? 需要实时干预 → 受控;只需要最终结果 → 非受控。
三、受控组件详解
3.1 基础用法:input / textarea / select
受控组件的公式:value 绑定 state + onChange 更新 state,两者成对出现。
function ControlledForm() {
const [name, setName] = useState("");
const [bio, setBio] = useState("");
const [color, setColor] = useState("blue");
return (
<form>
{/* input */}
<input
value={name} // 值由 state 决定
onChange={e => setName(e.target.value)} // 变化时更新 state
/>
{/* textarea —— 注意:React 用 value 而不是 children */}
<textarea value={bio} onChange={e => setBio(e.target.value)} />
{/* select —— value 绑在 select 上,不是 option 上 */}
<select value={color} onChange={e => setColor(e.target.value)}>
<option value="red">红色</option>
<option value="blue">蓝色</option>
<option value="green">绿色</option>
</select>
</form>
);
}
注意 textarea 和 select 的写法——React 统一用 value 属性,这和原生 HTML 不同(原生 textarea 用标签内容,select 用 selected 属性)。
3.2 多个输入:用对象 state 管理
表单字段多了,一个字段一个 useState 会疯掉。用一个对象统一管理:
function SignupForm() {
const [form, setForm] = useState({
username: "",
email: "",
password: "",
});
// 通用更新函数——利用 ES6 计算属性名
const handleChange = e => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
return (
<form>
<input name="username" value={form.username} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<input
name="password"
value={form.password}
onChange={handleChange}
type="password"
/>
</form>
);
}
关键技巧:name 属性和 state 的 key 同名,这样 handleChange 就能一个函数管所有字段。
3.3 实时验证模式
受控组件最大的优势:每次按键都能响应。
function UsernameInput() {
const [username, setUsername] = useState("");
const [error, setError] = useState("");
const handleChange = e => {
const val = e.target.value;
setUsername(val);
// 实时校验
if (val.length < 3) {
setError("用户名至少 3 个字符");
} else if (!/^[a-zA-Z0-9_]+$/.test(val)) {
setError("只能包含字母、数字和下划线");
} else {
setError("");
}
};
return (
<div>
<input value={username} onChange={handleChange} />
{error && <span className="error">{error}</span>}
</div>
);
}
受控组件的实时验证不是”额外功能”,而是数据流自然的结果——state 变了,UI 就跟着变。
3.4 “完全控制”:value + onChange 成对出现
受控组件的铁律:value 和 onChange 必须成对出现。
// ❌ 只写 value 不写 onChange —— 输入框将无法修改!
<input value={name} />
// React 会警告:你提供了 value 但没有 onChange,这个输入框是只读的
// ✅ 成对出现
<input value={name} onChange={e => setName(e.target.value)} />
// ✅ 如果确实想要只读,用 readOnly
<input value={name} readOnly />
为什么只写 value 会锁死输入?因为 React 把 value 当作唯一真相——你告诉它值是 name,它就永远是 name,用户打字也改不了(因为没有 onChange 告诉 React 更新 name)。
四、非受控组件详解
4.1 defaultValue:非受控的初始值
非受控组件不绑 value,而是用 defaultValue 设初始值,之后 DOM 自己管:
function UncontrolledInput() {
const inputRef = useRef(null);
return (
<input
defaultValue="初始内容" // 只在首次渲染生效
ref={inputRef} // 用 ref 拿到 DOM 节点
/>
);
}
defaultValue 和 value 的关键区别:
value:每次渲染都强制同步 → 受控defaultValue:只在挂载时设置一次 → 非受控
4.2 ref 读取值
非受控组件的核心操作:需要值的时候,从 ref 读。
function SimpleForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = () => {
// 提交时才读值
console.log(nameRef.current.value);
console.log(emailRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input defaultValue="" ref={nameRef} />
<input defaultValue="" ref={emailRef} />
<button type="submit">提交</button>
</form>
);
}
非受控组件的值”活在 DOM 里”——React 渲染树上看不到它,只有通过
ref才能摸到。
4.3 文件上传:必须非受控
<input type="file"> 是唯一必须用非受控的表单元素——因为 JavaScript 出于安全原因不能通过代码设置文件路径。
function FileUpload() {
const fileRef = useRef(null);
const handleSubmit = e => {
e.preventDefault();
const file = fileRef.current.files[0]; // 从 ref 读取文件
console.log("选中的文件:", file.name);
};
return (
<form onSubmit={handleSubmit}>
<input type="file" ref={fileRef} />
<button type="submit">上传</button>
</form>
);
}
// ❌ 文件输入没有 value 属性,不能受控
<input type="file" value={something} />
// ✅ 只能用 ref
<input type="file" ref={fileRef} />
4.4 集成非 React 库
当需要把非 React 库(如 jQuery 插件、地图 SDK)挂到 DOM 元素上时,非受控模式是自然选择——React 不去管那个 DOM 节点的值,让第三方库自己操作:
function DatePickerWrapper() {
const inputRef = useRef(null);
useEffect(() => {
// 假设 flatpickr 是一个 jQuery 日期选择器
flatpickr(inputRef.current, {
dateFormat: "Y-m-d",
onChange: dates => {
console.log("选择的日期:", dates[0]);
},
});
}, []);
return <input ref={inputRef} />; // React 不干预,flatpickr 自己管
}
非受控组件是 React 和”外部世界”的接口——当 DOM 不完全归 React 管时,用 ref 桥接。
五、何时选择哪个?
5.1 默认推荐受控
大多数场景,用受控组件。 原因:数据流更可预测,调试更容易。
受控组件让表单值始终在 React 的可视范围内——你可以在 React DevTools 里看到 state,可以在任何时刻校验、转换、条件渲染。数据流是单向的、可追踪的。
5.2 非受控的适用场景
以下情况非受控更合适:
| 场景 | 为什么非受控更好 |
|---|---|
| 文件上传 | JS 不能设置文件路径,必须非受控 |
| 简单一次性表单 | 只需提交时读值,不需要实时反馈,省代码 |
| 集成非 React 库 | 第三方库直接操作 DOM,React 不应干预 |
| 性能敏感的大量输入 | 受控组件每次按键都触发 setState → 重渲染;非受控跳过这个开销 |
关于性能的补充说明:大多数表单不会有性能问题。只有当表单有几百个输入字段,或者每次按键触发昂贵的重渲染时,非受控才有实际优势。不要过早优化。
// 性能敏感场景示例:大量输入字段
function MassiveForm() {
// ❌ 受控:每次按键都重渲染整个组件
// const [fields, setFields] = useState({ ...100个字段 })
// ✅ 非受控:只在提交时读值,避免频繁重渲染
const formRef = useRef(null);
const handleSubmit = e => {
e.preventDefault();
const formData = new FormData(formRef.current);
// 遍历所有字段值
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
...
</form>
);
}
六、表单提交处理
无论受控还是非受控,表单提交的处理方式是一样的:监听 onSubmit + e.preventDefault()。
function LoginForm() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = e => {
e.preventDefault(); // 阻止浏览器默认行为(页面刷新)
// 受控:从 state 读值
console.log({ username, password });
};
return (
<form onSubmit={handleSubmit}>
<input value={username} onChange={e => setUsername(e.target.value)} />
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<button type="submit">登录</button>
</form>
);
}
// ❌ 常见错误:忘记 preventDefault
const handleSubmit = (e) => {
// 没有 e.preventDefault(),页面会刷新,所有数据丢失
console.log(form)
}
// ❌ 把提交逻辑放在 button 的 onClick 上
<button onClick={handleSubmit}>提交</button>
// 问题:用户按 Enter 提交时不会触发 onClick
// ✅ 放在 form 的 onSubmit 上
<form onSubmit={handleSubmit}>
<button type="submit">提交</button>
</form>
// 这样无论是点击按钮还是按 Enter,都会触发
e.preventDefault()是表单提交的”安全带”——不系的话,浏览器会按默认行为刷新页面,你的 React 逻辑白跑了。
七、表单库简介
手写表单逻辑到一定复杂度后,你会想要库来帮忙。两个主流选择:
7.1 React Hook Form
- 基于非受控 + ref,不绑定 state,减少重渲染
- 性能好,适合大型表单
- 校验规则声明式定义,与 UI 解耦
7.2 Formik
- 基于受控组件,state 驱动表单值
- API 更直观,心智模型和原生 React 一致
- 适合中小型表单,需要实时反馈的场景
7.3 选择思路
不要纠结”哪个更好”,而是看你的场景:
- 表单大、字段多、性能敏感 → React Hook Form
- 表单逻辑简单、需要实时反馈 → Formik 或手写受控
- 刚学 React → 先手写受控,理解原理后再上库
两个库都只是减少样板代码的工具,底层原理还是受控/非受控。理解了本章内容,学任何表单库都不在话下。
八、常见陷阱
8.1 受控和非受控混用警告
// ❌ 同一个输入一会儿受控一会儿非受控
function BadInput() {
const [value, setValue] = useState(undefined); // undefined → 非受控
// 第一次渲染:value 是 undefined,React 认为是非受控
// 用户输入后 setValue → value 有值了,React 认为是受控
// 控制台警告:组件从非受控变为受控
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
// ✅ 初始值用空字符串,始终受控
function GoodInput() {
const [value, setValue] = useState(""); // 空字符串 → 受控
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
8.2 textarea 的 children 陷阱
// ❌ 原生 HTML 习惯:内容写在标签里
<textarea>初始内容</textarea>
// ✅ React 受控写法:用 value
<textarea value={text} onChange={e => setText(e.target.value)} />
// ✅ React 非受控写法:用 defaultValue
<textarea defaultValue="初始内容" ref={textRef} />
8.3 select 的 value 陷阱
// ❌ 原生 HTML 习惯:在 option 上加 selected
<select>
<option value="a">A</option>
<option value="b" selected>B</option> {/* React 中无效 */}
</select>
// ✅ React 受控写法:value 绑在 select 上
<select value={selected} onChange={e => setSelected(e.target.value)}>
<option value="a">A</option>
<option value="b">B</option>
</select>
九、决策流程图
面对一个表单字段,按这个顺序决策:
- 是文件上传吗? → 必须非受控
- 需要实时验证/条件渲染吗? → 受控
- 需要集成非 React 库吗? → 非受控
- 字段很多且性能敏感吗? → 考虑非受控或表单库
- 其他情况 → 默认受控
一句话:能受控就受控,不得不非受控才非受控。
一句话总结:受控组件让 React 掌控一切(state 驱动),非受控组件让 DOM 自治(ref 读取)——默认受控,文件上传和第三方集成用非受控,复杂表单上库。
相关:
- useState完全指南 — 受控组件的 state 基础
- useRef完全指南 — 非受控组件的 ref 基础
- React组件与函数本质 — 理解组件重渲染机制
- 自定义Hook设计 — 抽象表单逻辑为自定义 Hook