React 受控组件与非受控组件完全指南
引言
在上一篇文章中,我们深入学习了 useRef 的核心用法,掌握了如何通过它操作 DOM 元素以及保存跨渲染周期的数据(如定时器 ID)。这些能力在处理某些特定场景时不可或缺。而在日常开发中,还有一个与 useRef 紧密相关的高频需求------表单处理。
当我们在 React 中构建表单时,通常面临两种数据管理方式:受控组件 与 非受控组件 。其中,非受控组件正是借助 useRef 来直接获取 DOM 中的表单值,而受控组件则依赖 useState 实现数据与视图的同步。理解这两者的区别与适用场景,是写出高效、可维护表单代码的关键。
本文将从前一篇文章的知识点出发,系统讲解受控组件与非受控组件的核心概念、实战用法,并通过多个可运行示例,帮助你根据实际需求做出正确的技术选型。无论你是刚接触 React 的初学者,还是希望巩固基础的中级开发者,相信都能从中受益
一、初识受控与非受控:一个混合表单的例子
在深入讲解之前,我们先通过一个同时包含受控和非受控输入框的表单,直观感受它们的区别。
jsx
import { useState, useRef } from 'react';
export default function App() {
// 受控组件:值由 React state 管理
const [value, setValue] = useState('');
// 非受控组件:通过 ref 获取 DOM 值
const inputRef = useRef(null);
const doLogin = (e) => {
e.preventDefault(); // 阻止页面刷新
console.log('受控组件值:', value);
console.log('非受控组件值:', inputRef.current.value);
};
return (
<form onSubmit={doLogin}>
{/* 实时显示受控组件的值,方便观察变化 */}
<p>受控输入的值:{value}</p>
{/* 受控输入框:value 由 state 控制,onChange 更新 state */}
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="受控输入"
/>
{/* 非受控输入框:没有 value 属性,完全由 DOM 维护,仅用 ref 获取 */}
<input type="text" ref={inputRef} placeholder="非受控输入" />
<button type="submit">登录</button>
</form>
);
}
在这个例子中:
- 第一个
<input>是受控组件 :它的value属性绑定到valuestate,任何输入都会触发onChange更新 state,从而实现数据和视图的同步。同时,我们在表单上方实时展示了value的值。 - 第二个
<input>是非受控组件 :它没有设置value属性,用户输入直接反映在输入框内,我们通过ref在提交时读取它的值。 - 点击提交按钮,控制台会输出两个输入框的当前值。
通过这个混合示例,我们可以看到受控组件的数据流是"React state → 输入框",而非受控组件的数据流是"输入框 → DOM → 通过 ref 获取"。接下来我们将分别深入探讨这两种模式。
效果图
二、受控组件详解
2.1 什么是受控组件?
受控组件是指表单元素的值由 React 的 state 控制 的表单组件。具体来说,我们通过 value 属性(或 checked 属性,针对复选框等)将 state 绑定到表单元素,并通过 onChange 事件监听用户输入,从而更新 state。这样,组件的值始终与 state 同步,我们可以实时获取和验证用户输入。
2.2 受控组件示例:登录表单
下面是一个典型的受控组件示例(来自 LoginForm.jsx),它使用一个 state 对象统一管理多个表单项:
jsx
import { useState } from 'react';
export default function LoginForm() {
// 使用 state 统一管理表单数据
const [form, setForm] = useState({
username: '',
password: ''
});
// 处理输入变化:根据输入框的 name 动态更新对应的字段
const handleChange = (e) => {
// 使用计算属性名,实现一个函数处理所有输入
setForm({
...form, // 保留其他字段
[e.target.name]: e.target.value // 动态更新当前字段
});
};
// 处理表单提交
const handleSubmit = (e) => {
e.preventDefault(); // 阻止默认提交行为(页面刷新)
console.log('表单数据:', form); // 这里可以发送 AJAX 请求
// 例如:fetch('/api/login', { method: 'POST', body: JSON.stringify(form) })
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>用户名:</label>
<input
type="text"
name="username"
placeholder="请输入用户名"
value={form.username} // 值由 state 控制
onChange={handleChange} // 输入变化时更新 state
/>
</div>
<div>
<label>密码:</label>
<input
type="password"
name="password"
placeholder="请输入密码"
value={form.password}
onChange={handleChange}
/>
</div>
<button type="submit">注册</button>
</form>
);
}
2.3 受控组件的特点与优点
- 数据实时同步 :每当用户输入,
onChange触发,state 立即更新,数据始终与输入一致。 - 易于实现实时验证 :可以在
handleChange中添加校验逻辑,例如判断用户名长度、密码强度,并实时显示错误提示。 - 易于实现条件控制:可以根据 state 的值动态禁用按钮、显示隐藏元素等。
- 符合 React 单向数据流:数据从 state 流向视图,视图通过事件反馈更新 state,逻辑清晰。
2.4 受控组件的缺点
- 代码量稍多 :需要为每个表单元素定义 state 和
onChange处理函数。 - 性能考虑 :频繁的
setState可能在高频输入场景下引发性能问题(但通常可忽略不计)。
三、非受控组件详解
3.1 什么是非受控组件?
非受控组件是指表单元素的值由 DOM 自身维护 的表单组件。我们不需要为每个输入绑定 state 和 onChange,而是通过 ref 在需要时(例如提交时)直接读取 DOM 节点的值。这种方式更接近传统的 HTML 表单。
3.2 非受控组件示例:评论框
下面是一个非受控组件的示例(来自 CommentBox.jsx):
jsx
import { useRef } from 'react';
export default function CommentBox() {
// 创建 ref 用于关联 textarea 元素
const textareaRef = useRef(null);
const handleSubmit = () => {
// 通过 ref.current.value 直接读取 DOM 中的值
const comment = textareaRef.current.value;
if (!comment) {
return alert('请输入评论内容');
}
console.log('评论内容:', comment); // 这里可以发送 AJAX 请求
// 例如:fetch('/api/comments', { method: 'POST', body: JSON.stringify({ comment }) })
};
return (
<div>
<textarea
ref={textareaRef}
placeholder="请输入评论内容"
rows="4"
cols="50"
/>
<br />
<button onClick={handleSubmit}>提交</button>
</div>
);
}
3.3 非受控组件的特点与优点
- 代码简洁 :不需要定义 state 和
onChange,只需一个ref。 - 更接近传统 HTML:对于简单的表单或迁移旧代码,非受控组件更直观。
- 适用于特殊输入类型 :例如
<input type="file" />只能使用非受控组件,因为它的值是只读的,无法通过 React state 设置。 - 性能敏感场景 :避免因频繁
setState引起的渲染,适合大量输入且不需要实时反馈的场景。
3.4 非受控组件的缺点
- 实时验证困难:无法在输入过程中即时验证数据,只能在提交时验证。
- 数据获取时机受限:只能在需要时(如提交)通过 ref 获取值,无法随时访问。
- 不符合 React 数据流理念:数据源头是 DOM,与 React 的声明式编程风格略有冲突。
整体效果图

四、受控 vs 非受控:对比与选择
| 维度 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据源 | React state | DOM 自身 |
| 获取值方式 | 直接从 state 读取 | 需要时通过 ref 获取(如 ref.current.value) |
| 实时验证 | 容易实现(在 onChange 中添加逻辑) |
较难实现,通常在提交时验证 |
| 代码量 | 稍多(需要定义 state 和 onChange) |
较少(只需 ref) |
| 适用场景 | 复杂表单、需要实时反馈、联动、动态验证的表单 | 简单表单、一次性读取、文件上传、性能敏感场景 |
| 典型示例 | 登录注册、个人资料编辑 | 评论框、文件上传、搜索框(非即时搜索) |
如何选择?
- 优先选择受控组件:因为它符合 React 的设计理念,数据流清晰,便于维护和扩展。大多数业务场景下的表单都应使用受控组件。
- 在特定场景下使用非受控组件:例如文件上传(必须使用非受控)、需要集成第三方非 React 库(如富文本编辑器)、或者表单极其简单且不需要实时交互时。
五、重要补充:为什么要阻止表单默认提交?
在所有的表单示例中,你可能都看到了这样一行代码:
jsx
const handleSubmit = (e) => {
e.preventDefault();
// 其他逻辑...
};
这行代码的作用是阻止浏览器执行表单的默认提交行为 。如果不调用 preventDefault,当用户点击提交按钮时,浏览器会:
- 收集表单数据(根据
name属性)。 - 发送请求到表单的
action地址(默认为当前页面的 URL)。 - 重新加载页面(或跳转),导致整个页面的状态丢失。
在 React 单页应用(SPA)中,我们希望用 JavaScript 异步发送数据(AJAX/Fetch),在不刷新页面的情况下更新界面。因此,必须阻止默认行为,将控制权交给我们的代码。
六、总结
- 受控组件由 React state 驱动,数据与视图完全同步,适合需要实时交互和验证的复杂表单。它是 React 中推荐的表单处理方式。
- 非受控组件由 DOM 自身维护,通过 ref 获取值,代码简洁,适用于简单场景或必须直接操作 DOM 的情况(如文件上传)。
- 在实际开发中,优先考虑受控组件,当遇到不需要实时响应、仅在提交时读取值的场景时,再考虑非受控组件。
- 不要忘记在表单提交事件中调用
e.preventDefault(),以确保我们能够用 JavaScript 完全控制表单提交过程。
希望通过本文的讲解,你能对 React 中的受控与非受控组件有更清晰的认识,并能在实际项目中灵活运用。如果你有任何疑问或更好的见解,欢迎在评论区留言讨论!
