🌱 作者自白:
作为一个正走在“从小白到能打项目实战”的 React 学习者,你一定会遇到表单的各种花式输入框 —— 登录、注册、搜索、留言、上传、校验、联动……
看起来很基础,但这里面却藏着两个 React 世界观的终极哲学之争:受控组件(Controlled)和非受控组件(Uncontrolled) 。
如果你连它们都搞不清,表单做着做着,就会写得越来越混乱;如果你掌握了它们,React 表单将成为你代码中的一把利剑!
🌱 一、什么是受控组件?
“我掌控每一次输入的节奏,我知道它什么时候改变,也知道该不该让它改。”
这是 React 控制型表单的口吻。
📘 定义
受控组件指的是其表单值由 React 的 state 或其他状态管理机制所控制。
输入变化时,用户交互触发 onChange,进而触发 setState,然后 value 的值重新传回 input 元素。
✅ 代码示例
import { useState } from 'react';
function ControlledInput() {
const [username, setUsername] = useState('');
return (
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="请输入用户名"
/>
);
}
🔍 流程解析
- input 的
value绑定的是 React 状态; - 用户每输入一个字,都会调用
setUsername更新; - 组件重新渲染,展示新状态。
🚨 特别提醒
如果你不给 value,React 会警告你:“你写的是非受控组件,但你又用了受控行为(比如onChange)。”
🌾 二、什么是非受控组件?
“我信任浏览器,我只在需要时看看它的值,其他时候你别来烦我。”
这是非受控组件的态度。
📘 定义
非受控组件是由原生 DOM 自己维护表单值,React 不主动参与值更新过程。
读取值的方式是使用 ref 引用,直接操作 DOM 元素。
✅ 代码示例
import { useRef } from 'react';
function UncontrolledInput() {
const inputRef = useRef();
const handleSubmit = () => {
alert('你输入的是:' + inputRef.current.value);
};
return (
<>
<input type="text" ref={inputRef} />
<button onClick={handleSubmit}>提交</button>
</>
);
}
⚔️ 三、受控组件 vs 非受控组件,对比一览表
| 维度 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据来源 | React 的 state | DOM 本身 |
| 更新方式 | 每次输入更新状态 | 不自动更新 |
| 性能影响 | 输入时 re-render,略重 | 不依赖 render,性能优 |
| 验证能力 | 易于校验、联动等 | 需手动获取值再校验 |
| 状态管理 | 状态可控、可追踪 | 无法统一追踪多个字段 |
| 文件上传支持 | 不友好(file 类型无法受控) | 适合 |
| 第三方库支持 | ✅ Formik / RHF | ✅ react-hook-form 更适配非受控 |
| 场景适配 | 多用于验证、联动、复杂表单 | 多用于简单表单、文件、性能优化 |
🔬 四、到底应该怎么选?
React 官方建议:尽可能使用受控组件,它更贴合 React 的声明式思想、数据单向流动、更利于未来扩展和测试。
但在下面场景中,非受控组件是更佳选择:
- 文件上传(input[type="file"]);
- 不需要频繁响应的简单表单(例如只在点击按钮时读取值);
- 集成第三方组件(如 jQuery 插件、CKEditor、地图控件);
- 性能优化场景(避免 re-render 导致卡顿);
- 迁移老代码或混合栈。
🧪 五、项目实战对比:控制 vs 放养
场景 1:注册表单(使用受控组件)
需求:
- 用户名必须填写;
- 密码长度不少于 6;
- 禁止非法字符;
- 输入时显示提示;
function RegisterForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleRegister = () => {
if (!username || password.length < 6) {
alert('输入有误');
return;
}
console.log({ username, password });
};
return (
<>
<input value={username} onChange={(e) => setUsername(e.target.value)} placeholder="用户名" />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="密码" />
<button onClick={handleRegister}>注册</button>
</>
);
}
场景 2:文件上传表单(使用非受控组件)
function FileUploadForm() {
const fileRef = useRef();
const handleUpload = () => {
const file = fileRef.current.files[0];
console.log('选择的文件:', file);
};
return (
<>
<input type="file" ref={fileRef} />
<button onClick={handleUpload}>上传</button>
</>
);
}
🚧 六、受控组件中的隐藏陷阱
🕳️ 陷阱 1:空字符串和 undefined 的切换
<input value={undefined} /> // 报错,value 必须是 string
React 不允许你混用“受控”与“非受控”状态,初值必须是空字符串或定义好的默认值。
🕳️ 陷阱 2:多个 input 共用一个 state 容器
const [form, setForm] = useState({ name: '', age: '' });
<input
name="name"
value={form.name}
onChange={(e) =>
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }))
}
/>
这种结构写多了容易忘记拆解更新逻辑,建议用表单库管理。
🔧 七、常用表单工具对受控/非受控的支持
| 库 | 主要方式 | 优势 |
|---|---|---|
| Formik | 基于受控组件 | 成熟、文档完善、集成 Yup |
| react-hook-form | 基于非受控组件 + Proxy 技术 | 轻量、性能强、支持 TS、无需 re-render |
| Zod / Yup | 验证库 | 可配合 Formik/RHF,提供类型验证能力 |
🎤 八、面试必问问题拓展
❶ 什么是受控组件与非受控组件,它们的区别?
答:受控组件由 React 的状态控制输入值,通过 value 和 onChange 保持同步;非受控组件依赖 DOM 自身管理,使用 ref 读取值。受控组件更易于验证和联动,非受控适合性能敏感或文件上传等场景。
❷ React 中为什么推荐使用受控组件?
答:受控组件使得数据同步、表单验证、状态可追踪、组件调试更方便,符合 React 的声明式哲学。大型项目中也便于配合状态管理工具,如 Redux 或 Context。
❸ 实际项目中你使用过哪些表单管理工具?
答:我使用过 Formik 和 react-hook-form:
- Formik 更贴合受控思想,易读易写;
- react-hook-form 更轻量,适合性能要求较高的大表单,TS 体验也不错。
✍️ 九、总结一句话
“React 表单处理没有绝对的对错,只有更适合当前场景的选择。要响应联动,就用受控;要性能简单,就用非受控。 ”