React 中的受控组件与非受控组件详解
在 React 开发中,表单处理是一个常见但又容易混淆的话题。开发者常常会遇到“受控组件”(Controlled Components)和“非受控组件”(Uncontrolled Components)这两个概念。它们代表了两种不同的表单数据管理方式,各有适用场景和优劣。本文将深入解析这两种模式,并结合 useRef、useState 等 Hooks,帮助你掌握何时使用哪种方式。
一、什么是受控组件?
受控组件是指表单元素的值由 React 的状态(state)控制。换句话说,表单元素的 value 属性绑定到某个 state 变量上,并通过 onChange 事件同步更新该 state。
特点:
- 表单的值完全由 React 状态驱动(单向数据流)。
- 每次用户输入都会触发状态更新,进而重新渲染组件。
- 适合需要实时校验、联动、动态展示等复杂交互的场景。
示例:受控登录表单
import { useState } from 'react';
export default function LoginForm() {
const [form, setForm] = useState({
username: '',
password: ''
});
const handleChange = (e) => {
setForm({
...form,
[e.target.name]: e.target.value
});
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(form); // { username: '...', password: '...' }
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="请输入用户名"
name="username"
value={form.username}
onChange={handleChange}
/>
<input
type="password"
placeholder="请输入密码"
name="password"
value={form.password}
onChange={handleChange}
/>
<button type="submit">注册</button>
</form>
);
}
在这个例子中,<input> 的值始终由 form 状态决定,任何输入都会通过 handleChange 更新状态,实现双向绑定的效果(虽然 React 本质是单向数据流)。
二、什么是非受控组件?
非受控组件则是让 DOM 自己管理表单数据,React 不直接控制其值。通常通过 ref 来在需要时读取当前的 DOM 值。
特点:
- 表单元素的值存储在 DOM 中,而非 React 状态。
- 使用
useRef获取 DOM 节点,通过.current.value读取值。 - 适合一次性读取(如提交时)、性能敏感或文件上传等场景。
示例:非受控评论框
import { useRef } from 'react';
export default function CommentBox() {
const textareaRef = useRef(null);
const handleSubmit = () => {
const comment = textareaRef.current.value;
if (!comment) return alert('请输入评论');
console.log(comment);
};
return (
<div>
<textarea ref={textareaRef} placeholder="输入评论···" />
<button onClick={handleSubmit}>提交</button>
</div>
);
}
这里没有使用 value 和 onChange,而是直接通过 ref 在提交时获取值,避免了不必要的状态更新。
三、受控 vs 非受控:如何选择?
| 场景 | 推荐方式 |
|---|---|
| 实时校验(如密码强度、格式检查) | ✅ 受控组件 |
| 多字段联动(如省市区选择) | ✅ 受控组件 |
| 表单值需频繁用于 UI 更新 | ✅ 受控组件 |
| 一次性提交(如简单留言) | ✅ 非受控组件 |
文件上传 <input type="file"> | ✅ 非受控组件(必须) |
| 性能敏感、避免频繁重渲染 | ✅ 非受控组件 |
⚠️ 注意:
<input type="file">是只读的,无法通过value设置,因此只能是非受控组件。
四、useRef 的核心作用
useRef 是 React 提供的一个 Hook,用于创建一个可变且持久的引用对象,其 .current 属性可被读写,但不会触发组件重新渲染。
与 useState 的对比:
| 特性 | useState | useRef |
|---|---|---|
| 是否触发重渲染 | ✅ 是 | ❌ 否 |
| 用途 | 管理响应式状态 | 存储可变值、引用 DOM |
| 初始值 | useState(initial) | useRef(initial) |
| 更新方式 | setState(newValue) | ref.current = newValue |
useRef 的典型应用场景:
-
访问 DOM 元素
const inputRef = useRef(null); useEffect(() => { inputRef.current.focus(); // 自动聚焦 }, []); -
存储定时器 ID(避免闭包问题)
const intervalId = useRef(null); const start = () => { intervalId.current = setInterval(() => console.log('tick'), 1000); }; const stop = () => { clearInterval(intervalId.current); }; -
保存上一次的状态(配合 useEffect)
(常用于比较 prev/next 值)
五、混合使用:一个组件中同时存在受控与非受控
实际开发中,一个表单可能部分字段需要实时校验(受控),另一部分只需提交时读取(非受控)。React 允许混合使用:
import { useState, useRef } from 'react';
export default function App() {
const [username, setUsername] = useState(''); // 受控
const passwordRef = useRef(null); // 非受控
const handleSubmit = (e) => {
e.preventDefault();
console.log('用户名:', username);
console.log('密码:', passwordRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="用户名(受控)"
/>
<input
type="password"
ref={passwordRef}
placeholder="密码(非受控)"
/>
<button type="submit">登录</button>
</form>
);
}
💡 虽然可行,但建议保持一致性,避免逻辑混乱。
六、总结
- 受控组件:状态驱动,适合复杂交互,代码更“React 式”。
- 非受控组件:DOM 驱动,适合简单场景,性能更优。
useRef:不是状态管理工具,而是“默默奉献”的引用容器,用于 DOM 访问或存储可变值而不触发渲染。
在实际项目中,优先考虑受控组件,因为它更符合 React 的声明式理念;但在特定场景下(如文件上传、性能优化),非受控组件是更合理的选择。
掌握两者的区别与适用场景,是写出高效、可维护 React 表单的关键一步。
📌 最佳实践建议:
对于大多数业务表单(登录、注册、设置等),使用受控组件 + 表单验证库(如 Formik、React Hook Form)是更稳健的方案。而像搜索框、评论框等轻量输入,可酌情使用非受控以简化逻辑。