React受控组件和非受控组件有什么区别?

作者:晚风写码

时间:2026-11-12

浏览:341

深入对比React受控组件与非受控组件的核心区别、数据流向模型、代码实现、选型决策树与常见避坑指南。

文章目录
在 React 表单开发中,受控组件(Controlled Component)与非受控组件(Uncontrolled Component)的核心区别在于表单数据的单一数据源(Single Source of Truth)由谁保管:受控组件的状态由 React 的 state 全权掌控,任何输入都通过 onChange 触发更新并反写回 DOM;非受控组件的数据则直接保存在原生 DOM 节点内部,React 仅在需要时通过 useRef 从 DOM 读取数据。
React受控组件和非受控组件有什么区别?
图 0:React 受控组件与非受控组件核心机制概览

一、核心概念:表单数据的“控制权”在谁手里

在传统网页开发中,表单输入框(如 <input>、<textarea>、<select>)是天生有内部状态的,用户每敲击一次键盘,浏览器就会在原生 DOM 元素内部更新它的 value。

当我们在 React 中处理表单时,就面临一个核心设计抉择:是让 React 接管这个输入框的实时状态,还是继续让原生 DOM 自身保存数据?由此衍生出了两种截然不同的组件模式:

  • 受控组件:把表单元素的值绑定到 React 的 state。输入框不再自行决定当前展示什么文字,而是严格依据 React 传入的 value 属性进行渲染。用户输入时,必须通过 onChange 事件通知 React 调用 setState,更新状态后驱动组件重新渲染。
  • 非受控组件:让表单元素保持其原生的 DOM 行为。React 不通过 value 控制它,而是给它一个可选的 defaultValue 设置初始值。当需要使用表单数据(例如点击“提交”按钮)时,React 通过 ref 直接访问底层 DOM 节点来读取当前数值。
React受控组件和非受控组件有什么区别?
图 1:受控组件的闭环双向绑定数据流 vs 非受控组件的 DOM 自治与按需拉取

二、代码对比:同一输入功能的两种实现范式

为了直观理解这两种模式的区别,我们来看同一个用户名字段在两种模式下的完整代码实现。

1. 受控组件写法

import React, { useState } from 'react';

function ControlledInput() {
  // 1. 声明由 React 托管的状态
  const [username, setUsername] = useState('');

  const handleChange = (e) => {
    // 2. 在每次按键触发时更新 React 状态
    setUsername(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    // 3. 提交时直接使用已同步的 state,无需查询 DOM
    console.log('提交用户名:', username);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        用户名:
        <input
          type="text"
          value={username}
          onChange={handleChange}
        />
      </label>
      <button type="submit">提交</button>
    </form>
  );
}

在受控组件中,<input> 上的 value 属性被牢牢绑定在 username 状态上。用户每次键入一个字母,都会触发 handleChange,更新 username,进而导致组件重新渲染。如果我们在 handleChange 中写 setUsername(e.target.value.toUpperCase()),输入框甚至能立即将小写转换成大写。

2. 非受控组件写法

import React, { useRef } from 'react';

function UncontrolledInput() {
  // 1. 创建 DOM 引用容器
  const inputRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    // 2. 在提交时刻单向读取原生 DOM 节点的实际当前值
    const currentVal = inputRef.current ? inputRef.current.value : '';
    console.log('提交用户名:', currentVal);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        用户名:
        <input
          type="text"
          defaultValue="访客"
          ref={inputRef}
        />
      </label>
      <button type="submit">提交</button>
    </form>
  );
}

在非受控组件中,没有 useState,输入框只接收一个 defaultValue="访客" 作为挂载时的初值。用户输入时,React 完全不过问,组件也不会重新渲染;等到表单提交时,通过 inputRef.current.value 直接去 DOM 树上“取回”当前填写的文字。

React受控组件和非受控组件有什么区别?
图 2:受控组件的四步实现流程与非受控组件的三步实现流程

三、核心差异多维度对比

了解了两者的基本运作机制后,我们可以从数据流向、渲染性能、交互控制等多个维度详细剖析它们的本质差异:

特性维度 受控组件 (Controlled) 非受控组件 (Uncontrolled)
数据保存位置 React State 内存中(单一数据源) 浏览器原生 DOM 节点内部(DOM自治)
核心属性/API value + onChange defaultValue + ref
组件重渲染频率 用户每次击键都触发一次重新渲染 用户击键不触发组件重新渲染
即时输入校验 非常简单,可基于 state 派生错误提示 较难实现,需手动添加原生监听或在提交时处理
输入值强制格式化 支持(例如:自动去除空格、强制大写) 难以支持,DOM 内部值不受 React 约束
多字段联动 极度灵活(例如选择国家自动重置城市选项) 需要手动操作多个 ref 甚至直接修改 DOM
动态禁用提交按钮 直接根据 !text.trim() 控制 disabled 无法即时感知输入长度,难以动态切换按钮状态
集成第三方非React库 较复杂(需保持 React 状态与外部插件同步) 极简(可直接将 DOM ref 交给外部 jQuery/图表库)

四、选型指南:什么时候用受控,什么时候用非受控?

很多初学者常有一个误区,认为“受控组件才是 React 推荐的现代写法,非受控组件已经过时了”。这种观点是不准确的。在实际工程中,它们各有其不可替代的场景。

React受控组件和非受控组件有什么区别?
图 3:表单字段技术选型决策树

1. 必须或强烈建议使用受控组件的场景

  • 即时输入反馈与行内校验:例如用户输入密码时,实时显示“弱/中/强”强度条,或者在用户输错邮箱格式的瞬间显示红色提示。
  • 输入值的强制规范或过滤:例如银行卡号输入框,每隔4位自动插入空格;或者限制用户只能键入纯数字字符。
  • 多字段动态联动:例如当“收货方式”勾选“自提”时,立即清空并隐藏“配送地址”字段。
  • 依赖输入状态的 UI 控制:例如只有当表单所有必填字段均校验通过后,底部的“下一步”或“确认提交”按钮才从置灰状态变为高亮可用。

2. 适合或必须使用非受控组件的场景

  • 文件上传控件(<input type="file">):在 HTML 中,文件上传输入框出于浏览器安全限制,其 value 是严格只读的,React 无法通过 state 去程序化赋值。因此,文件上传控件在 React 中天然必须是非受控组件。
  • 接入传统非 React 原生 DOM 插件:例如引入了旧项目的富文本编辑器、日期范围选择器或图表插件,这些插件需要直接接管并操控底层 DOM 节点。
  • 超大型表单或动态表格的性能极限调优:如果一个单页包含数百个甚至上千个输入字段,使用受控组件可能导致每次敲击键盘都触发大范围组件树的 Diff 比较。使用非受控组件(或基于非受控机制构建的 react-hook-form 库)可以消除击键时的重渲染开销。
  • 极度简单的一次性查询表单:例如全站顶部的简单搜索栏,只需要在用户敲击回车或点击放大镜时获取文字,无需任何中间交互,使用非受控组件能节省大量样板代码。

五、常见陷阱与避坑指南

陷阱一:组件由“非受控”意外变为“受控”(A component is changing an uncontrolled input to be controlled)

这是 React 开发者最常遇到的控制台警告之一。出现这一报错的根本原因是:给 value 传递的初始状态是 undefined。

// 错误示范:初始值为 undefined
const [email, setEmail] = useState(); // 此时 email 为 undefined
<input value={email} onChange={e => setEmail(e.target.value)} />

当 React 首次渲染时,发现 value={undefined},React 会将该输入框当作非受控组件处理;当用户敲击键盘触发 onChange,email 变成了字符串(如 "a"),输入框突然收到了有明确值的 value 属性,变成了受控组件。React 会立刻抛出警告。
正确解法:受控组件的初始值绝不能省略,必须确保初始值为空字符串 '' 或其他确定的非 undefined 值:

// 正确做法:显式初始化为空字符串
const [email, setEmail] = useState('');
// 或者加兜底保护
<input value={email || ''} onChange={e => setEmail(e.target.value)} />
陷阱二:非受控组件的 defaultValue 不响应 props 更新

在非受控组件中,defaultValue 顾名思义仅在组件初次挂载(Mount)时生效。如果父组件后续更新了传入的 prop,非受控输入框内部的值不会自动同步更新。
如果业务场景需要根据传入的 ID 重新重置表单,最优雅的 React 模式是利用 key 属性重置组件实例:

// 当 userId 发生改变时,强制销毁旧 DOM 并重新挂载新 DOM 实例
<UserProfileEditor key={userId} initialName={user.name} />

六、全景对比与技术选型总结

受控组件与非受控组件并非非黑即白的技术对立,而是针对不同交互复杂度与性能诉求所提供的设计模式。

React受控组件和非受控组件有什么区别?
图 4:受控组件与非受控组件技术全景总结矩阵

总结而言:

  1. 优先原则:在绝大多数常规业务表单中,优先采用受控组件。它能够让表单数据完全融入 React 声明式、可预测的数据流中,使状态管理、调试跟踪和自动化测试都更加简单直观。
  2. 实用原则:遇到文件上传输入框(<input type="file">)、接入第三方原生插件、或者在大型复杂表单中追求极致击键性能时,果断采用非受控组件(或搭配 react-hook-form 等业界成熟方案),避免过度复杂化状态管理。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章