📝 受控组件 vs 非受控组件:React 表单的两种核心模式

35 阅读4分钟

在 React 开发中,处理用户输入(如登录、评论、注册等)是高频场景。而如何管理表单数据,React 提供了两种经典模式:受控组件(Controlled Component)非受控组件(Uncontrolled Component)

本文将结合你编写的 LoginForm(受控)和 CommentBox(非受控)两个组件,深入剖析两者的原理、差异、适用场景,并附上大厂常考的面试题,助你夯实基础、从容应对面试。


一、从代码看本质:两种写法对比

image.png image.png

image.png

image.png

✅ 1. 受控组件:LoginForm(使用 useState


const [form, setForm] = useState({ username: "", password: "" });

const handleChange = (e) => {
  setForm({
    ...form,
    [e.target.name]: e.target.value
  });
};

// JSX
<input 
  name="username"
  value={form.username}
  onChange={handleChange}
/>
  • 核心机制:表单的值由 React state 控制。
  • 数据流:用户输入 → 触发 onChange → 更新 state → 组件 re-render → 输入框显示新值。
  • 特点实时同步、状态驱动 UI

💡 这就是你在注册页面看到“用户名不能为空”、“密码长度至少6位”等实时校验提示的技术基础。


✅ 2. 非受控组件:CommentBox(使用 useRef


const textareaRef = useRef(null);

const handleSubmit = () => {
  const comment = textareaRef.current.value; // 提交时才读取
};

// JSX
<textarea ref={textareaRef} />
  • 核心机制:表单的值由 DOM 自身管理,React 不干预。
  • 数据流:用户输入 → DOM 自动更新 → 提交时通过 ref 一次性读取。
  • 特点延迟读取、操作 DOM

💡 类似于你在论坛发帖:你写完一大段话,点“提交”才把内容发出去,中间没有校验或联动。


二、关键区别:一张表说清楚

对比维度受控组件非受控组件
状态存储位置React state (useState)DOM 元素自身
如何获取值直接读 state(如 form.username通过 ref.current.value 读 DOM
是否实时响应✅ 是(每次输入都触发更新)❌ 否(仅在需要时读取)
能否做实时校验✅ 能(如密码强度、格式检查)❌ 不能
是否支持字段联动✅ 能(如确认密码必须一致)❌ 很难
代码复杂度略高(需写 handler)简洁(无需 state)
典型场景登录/注册、设置页、复杂表单评论框、搜索框、文件上传

三、为什么注册页面多用“受控组件”?

当你在大厂产品(如微信、淘宝、Google)的注册页输入账号时,常见以下交互:

  • 输入邮箱 → 实时提示“格式不正确”
  • 密码输入 → 显示“强度:弱/中/强”
  • 用户名输入 → 自动检测“该用户名已被占用”
  • 点击“下一步”前,按钮是禁用的,直到所有字段合法

这些功能都依赖“受控组件”

  • 因为只有把每个字段的值放在 state 中,才能在 useEffect 或 handleChange 中实时监听变化并做出反馈

而非受控组件无法做到这一点——它连“用户刚输了一个字”都不知道。


四、非受控组件的不可替代场景

虽然受控是主流,但非受控在以下场景更合适甚至必须使用

1. 文件上传

<input type="file" ref={fileRef} />
// ❌ 不能写 value={file},浏览器出于安全禁止程序设置 file input 的值

2. 简单一次性表单

  • 内部工具的快速反馈
  • 搜索关键词(只需提交时读一次)
  • 评论、留言等低交互场景

3. 性能敏感的高频输入

  • 如游戏聊天框、日志输入(避免频繁 re-render)

五、最佳实践建议

场景推荐方案
需要校验、联动、动态 UI✅ 受控组件(useState + onChange
简单提交、无实时反馈⚠️ 非受控组件(useRef
文件上传🔒 必须非受控
表单字段多、逻辑复杂✅ 考虑 react-hook-form(基于非受控优化性能,但提供受控式 API)

💡 小技巧:受控组件中,务必使用函数式更新避免闭包问题:



setForm(prev => ({ ...prev, [name]: value }));

六、大厂高频面试题(附答案要点)

❓ 1. React 中受控组件和非受控组件的区别是什么?

  • 受控组件的值由 React state 控制,通过 value + onChange 实现;
  • 非受控组件的值由 DOM 自身管理,通过 ref 读取;
  • 受控适合实时校验,非受控适合一次性读取。

❓ 2. 为什么 <input type="file"> 必须用非受控组件?


出于安全考虑,浏览器禁止 JavaScript 程序化设置 file 类型 input 的 value 属性,因此无法用 useState 控制其值,只能通过 ref 读取用户选择的文件。


❓ 3. 受控组件每次输入都会 re-render,会不会有性能问题?


在绝大多数业务场景中,现代 React(尤其是 Concurrent Mode)的优化足以应对。若真存在性能瓶颈(如每帧输入),可考虑:

  • 使用 useCallback 优化 handler
  • 节流/防抖更新 state
  • 或改用 react-hook-form 等库(底层非受控,上层提供受控体验)

❓ 4. 如何在受控组件中避免“闭包过期”问题?


使用 函数式更新



setForm(prev => ({ ...prev, [name]: value }));

而不是直接依赖当前 render 的 state 变量。


七、总结

  • 受控组件 = 状态驱动 + 实时响应 → 适合复杂表单
  • 非受控组件 = DOM 驱动 + 延迟读取 → 适合简单/一次性操作

你写的 LoginFormCommentBox 正好完美展示了这两种模式的典型应用。理解它们的差异,不仅能写出更健壮的代码,也能在面试中展现你对 React 核心思想的掌握。

记住
“要交互,用受控;要简单,用非受控。”