React 受控组件 vs 非受控组件:一篇吃透表单处理精髓
在 React 开发中,表单处理是高频场景——登录注册、评论提交、信息录入,几乎每个项目都会用到。但很多新手都会困惑:同样是获取表单输入值,为什么有的用 useState,有的用 useRef?这其实对应了 React 表单处理的两种核心方式:受控组件 和 非受控组件。
很多人分不清两者的区别,盲目使用导致表单出现“无法输入”“值获取不到”“性能冗余”等问题。本文将从「核心疑问出发」,拆解两者的定义、用法、区别,结合实战代码演示,帮你彻底搞懂什么时候用受控、什么时候用非受控,看完直接落地项目。
一、核心疑问:怎么拿到 React 表单的值?
原生 HTML 中,我们可以通过 DOM 直接获取表单元素的值,比如 document.querySelector('input').value。但 React 遵循“单向数据流”原则,不推荐直接操作 DOM,因此提供了两种更规范的方式获取表单值,对应两种组件类型。
先看一个最基础的示例,直观感受两者的差异:
import { useState, useRef } from 'react';
export default function App() {
// 受控组件:用状态控制输入框
const [value, setValue] = useState("")
// 非受控组件:用 ref 获取 DOM 值
const inputRef = useRef(null);
// 表单提交逻辑
const doLogin = (e) => {
e.preventDefault(); // 阻止页面刷新
console.log("非受控输入值:", inputRef.current.value); // 非受控获取值
console.log("受控输入值:", value); // 受控获取值
}
return (
<form onSubmit={
{/* 受控输入框:value 绑定状态,onChange 更新状态 */}
<input
type="text"
value={) => setValue(e.target.value)}
placeholder="受控输入框"
/>
{/* 非受控输入框:ref 关联 DOM,无需绑定状态 */}
<input
type="text"
ref={受控输入框"
style={{ marginLeft: '10px' }}
/>
<button type="submit" style={提交
)
}
上面的代码中,两个输入框分别对应受控和非受控两种方式,核心差异在于「值的控制者」不同——一个由 React 状态控制,一个由 DOM 原生控制。
二、逐字拆解:什么是受控组件?
1. 核心定义
受控组件:表单元素的值由 React 状态(useState)完全控制,输入框的显示值 = 状态值,输入行为通过 onChange 事件更新状态,从而实现“状态 ↔ 输入框”的联动。
核心逻辑:状态驱动 DOM,符合 React 单向数据流原则——数据从状态流向 DOM,DOM 输入行为通过事件反馈给状态,形成闭环。
2. 核心用法(必记)
实现一个受控组件,必须满足两个条件:
- 给表单元素绑定
value={状态值},让状态决定输入框显示内容; - 绑定
onChange事件,通过e.target.value获取输入值,调用 setState 更新状态。
3. 实战:多字段受控表单(登录注册场景)
实际开发中,表单往往有多个字段(如用户名、密码),此时可以用一个对象状态管理所有字段,配合事件委托简化代码:
import { useState } from "react"
export default function LoginForm() {
// 用对象状态管理多个表单字段
const [form, setForm] = useState({
username: "",
password: ""
});
// 统一处理所有输入框的变化
const handleChange = (e) => {
// 解构事件目标的 name 和 value(输入框需设置 name 属性)
const { name, value } = e.target;
// 更新状态:保留原有字段,修改当前输入字段(不可直接修改原对象)
setForm({
...form, // 展开原有表单数据
[name]: value // 动态更新对应字段
})
}
// 表单提交
const handleSubmit = (e) => {
e.preventDefault();
// 直接从状态中获取所有表单值,无需操作 DOM
console.log("表单数据:", form);
// 实际开发中:这里可做表单校验、接口请求等逻辑
}
return (
<form onSubmit={<div style={<input
type="text"
placeholder="请输入用户名"
name="username" /Change}
value={form.username} // 绑定状态值
style={{ padding: '6px' }}
/>
<div style={>
<input
type="password"
placeholder="请输入密码"
name="password" / 绑定状态值
style={{ padding: '6px' }}
/>
<button type="submit" style={注册
)
}
4. 受控组件的关键细节
- ⚠️ 只写
value={状态}不写onChange,输入框会变成「只读」——因为状态无法更新,输入框值永远固定; - 状态更新是异步的,但不影响表单输入(React 会批量处理状态更新,保证输入流畅);
- 适合做「实时操作」:比如实时表单校验、输入内容实时展示、表单字段联动(如密码强度提示)。
三、逐字拆解:什么是非受控组件?
1. 核心定义
非受控组件:表单元素的值由 DOM 原生控制,React 不干预输入过程,而是通过 useRef 获取 DOM 元素,再读取其 current.value 获取输入值。
核心逻辑:DOM 驱动数据,和原生 HTML 表单逻辑一致,React 只做“被动获取”,不主动控制输入值。
2. 核心用法(必记)
实现一个非受控组件,只需一步:
- 用
useRef(null)创建 Ref 对象,绑定到表单元素的ref属性; - 需要获取值时,通过
ref.current.value读取(通常在提交、点击等事件中获取)。
可选:用 defaultValue 设置初始值(仅首次渲染生效,后续修改不影响)。
3. 实战:非受控评论框(一次性提交场景)
评论框、搜索框等“一次性提交”场景,无需实时监控输入,用非受控组件更简洁高效:
import { useRef } from 'react';
export default function CommentBox() {
// 创建 Ref 对象,关联 textarea 元素
const textareaRef = useRef(null);
// 提交评论逻辑
const handleSubmit = () => {
// 防御性判断:避免 ref.current 为 null(极端场景)
if (!textareaRef.current) return;
// 获取输入值
const comment = textareaRef.current.value.trim();
// 表单校验
if (!comment) return alert('请输入评论内容!');
// 提交逻辑
console.log("评论内容:", comment);
// 提交后清空输入框(直接操作 DOM)
textareaRef.current.value = "";
}
return (
<div style={<textarea
ref={ placeholder="输入评论..."
style={{ width: '300px', height: '100px', padding: '10px' }}
defaultValue="请输入你的看法..." // 初始值(可选)
/>
<button
onClick={={{ padding: '6px 16px', marginTop: '10px' }}
>
提交评论
)
}
4. 非受控组件的关键细节
- ⚠️ 不要用
value绑定状态(否则会变成受控组件),初始值用defaultValue; - Ref 对象的
current在组件首次渲染后才会指向 DOM,因此不能在组件渲染时直接读取textareaRef.current.value(会报错); - 适合做「一次性操作」:比如文件上传( 必须用非受控)、简单搜索框、一次性提交的表单。
四、核心对比:受控组件 vs 非受控组件(必背)
很多人纠结“该用哪个”,其实核心看「是否需要实时控制输入」,用表格清晰对比两者差异,一目了然:
| 对比维度 | 受控组件 | 非受控组件 |
|---|---|---|
| 值的控制者 | React 状态(useState) | DOM 原生控制 |
| 核心依赖 | useState + onChange | useRef |
| 值的获取方式 | 直接读取状态(如 form.username) | ref.current.value |
| 初始值设置 | useState 初始值(如 useState("")) | defaultValue 属性 |
| 是否触发重渲染 | 输入时触发(onChange 更新状态) | 输入时不触发(无状态变化) |
| 适用场景 | 实时校验、表单联动、实时展示 | 一次性提交、文件上传、性能敏感场景 |
| 优点 | 可实时控制,符合 React 单向数据流,易维护 | 简洁高效,无需频繁更新状态,性能更好 |
| 缺点 | 频繁触发重渲染,代码量稍多 | 无法实时控制,需手动操作 DOM,不易做联动 |
五、实战总结:什么时候该用哪个?(重点)
不用死记硬背,记住两个核心原则,就能快速判断:
1. 优先用受控组件的情况
- 表单需要「实时校验」(如用户名长度限制、密码强度提示);
- 表单字段需要「联动」(如勾选“记住密码”才显示“密码确认”);
- 需要「实时展示输入内容」(如输入时同步显示剩余字符数);
- 表单数据需要和其他组件共享、联动(如跨组件传递表单值)。
2. 优先用非受控组件的情况
- 表单是「一次性提交」(如评论、搜索,无需实时监控);
- 需要处理「文件上传」( 是天然的非受控组件,无法用状态控制);
- 追求「性能优化」(避免频繁的状态更新和组件重渲染);
- 简单表单(如单个输入框,无需复杂逻辑)。
3. 避坑提醒
- 不要混合使用:同一个表单元素,不要既绑定
value又绑定ref,会导致逻辑混乱; - 非受控组件必做防御:获取值时,先判断
ref.current是否存在,避免报错; - 多字段表单优先受控:用对象状态管理,代码更规范、易维护。
六、最终总结
受控组件和非受控组件没有“谁更好”,只有“谁更合适”:
✅ 受控组件是 React 表单处理的「主流方式」,符合单向数据流,适合复杂表单、需要实时控制的场景;
✅ 非受控组件更「简洁高效」,贴近原生 HTML,适合简单场景、性能敏感场景和文件上传;
记住:判断的核心是「是否需要实时控制输入值」。掌握两者的用法和区别,就能轻松应对 React 中的所有表单场景,写出简洁、高效、可维护的代码。