React 受控组件与非受控组件

一、从生活出发:什么是"控制"?

想象你正坐在一辆汽车里。如果这辆车是自动驾驶 的,你只需要告诉它目的地,它自己会决定什么时候加速、什么时候转弯、什么时候刹车------你不需要时刻握着方向盘。但如果这辆车是手动驾驶 的,那么每一刻的车速、每一次转向,都需要你亲手操作,汽车的状态与你手中的方向盘、脚下的踏板实时同步

现在,把这个类比迁移到网页开发中。当你在网页上看到一个输入框(input),你在里面打字,字符一个一个地显示出来------你有没有想过,这些字符是怎么"出现"在屏幕上的?

有的输入框像自动驾驶汽车:它自己管理自己的内容,React 只在需要的时候(比如点击提交按钮)才去"看一眼"它写了什么。这就是非受控组件(Uncontrolled Component)

有的输入框像手动驾驶汽车:它的每一个字符都被 React "盯"着,你敲下一个键,React 立刻捕获这个变化,更新自己的内部状态,然后把新的值重新渲染到输入框中。这就是受控组件(Controlled Component)

这篇文章将带你从零开始,理解 React 中这两个核心概念。不需要你有很深的编程基础,只要了解最基本的 React 语法(比如什么是组件、什么是 JSX),你就能读懂。

二、前置知识:React 组件与 Hooks 三分钟速览

在深入正题之前,让我们花三分钟回顾一下必备的基础知识。

React 组件本质上就是一个函数,这个函数返回一段 HTML(准确说是 JSX)。比如:

jsx 复制代码
function Hello() {
  return <h1>你好,世界!</h1>;
}

这就是一个最简单的 React 组件。它在页面上渲染一个标题。

Hooks 是 React 16.8 引入的革命性特性,它让你在函数组件中"记住"一些东西。这篇文章会涉及两个最常用的 Hook:

  • useState :给组件添加"状态记忆"。调用 useState 会返回一个值和一个修改这个值的函数。每当你调用修改函数,React 就会用新的值重新渲染组件。
jsx 复制代码
const [count, setCount] = useState(0);
// count 是当前值,setCount 是修改它的函数
// 调用 setCount(5) 后,count 变成 5,组件重新渲染
  • useRef :创建一个"引用",你可以把它想象成一个标签,贴在页面上的某个 DOM 元素上。之后你就可以通过这个标签找到那个元素,直接读取它的属性。关键区别:修改 ref 的值不会触发组件重新渲染。
jsx 复制代码
const inputRef = useRef(null);
// 把它贴在 <input ref={inputRef}> 上
// 之后用 inputRef.current.value 就能读到输入框的当前内容

好,基础知识就绪。现在让我们正式进入受控与非受控的世界。


三、受控组件:React 是唯一的事实来源

3.1 核心思想

受控组件的核心哲学可以用一句话概括:组件的状态是唯一的真相来源(Single Source of Truth)

在受控组件中,表单元素(如 <input><textarea><select>)的值完全由 React 的状态(state)控制。用户输入 → 触发事件 → 更新 state → React 重新渲染组件 → 输入框显示新的值。这是一个闭环。

3.2 最简单的受控组件

让我们看一个项目中真实的例子------ControlledInput 组件:

jsx 复制代码
import { useState } from 'react';

function ControlledInput() {
  const [value, setValue] = useState('');

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
    </>
  );
}

让我们逐行理解这段代码:

  1. const [value, setValue] = useState(''):创建一个状态 value,初始值为空字符串。
  2. value={value}:这是关键!输入框的 value 属性被绑定到了 React 的状态上。这意味着输入框里显示什么,完全由 value 这个状态决定
  3. onChange={(e) => setValue(e.target.value)}:当用户在输入框中键入任何内容时,onChange 事件被触发,我们立即调用 setValue 把最新的输入内容存到状态中。

那数据流是什么样的呢?假设你在输入框中敲下了字母 "a":

  • 你的按键触发了 onChange 事件
  • e.target.value 此时是 "a"
  • setValue("a") 被调用,value 状态更新为 "a"
  • React 检测到状态变化,重新渲染组件
  • 输入框的 value 属性现在是 "a",所以你看到了 "a"

你可能会问:"这看起来绕了一大圈,为什么不让输入框自己管理自己的内容呢?" 好问题!答案在下一节的示例中揭晓。

3.3 受控组件的威力:实时表单验证

受控组件真正的威力不在于简单的输入框,而在于你可以在每一个输入变化的瞬间介入处理 。来看项目中的 LoginForm 组件:

jsx 复制代码
function LoginForm() {
  const [form, setForm] = useState({
    username: "",
    password: ""
  });
  const [errors, setErrors] = useState({});

  const validate = (name, value) => {
    let msg = "";
    if (name === 'username') {
      if (!value) {
        msg = '用户名为空';
      } else if (value.length < 3) {
        msg = '用户名长度不能小于3';
      }
    }
    if (name === 'password') {
      if (!value) {
        msg = '密码不能为空';
      } else if (value.length < 6) {
        msg = '密码长度不能小于6位';
      }
    }
    setErrors(prev => ({
      ...prev,
      [name]: msg
    }));
  };

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

  const isValid = form.username && form.password
    && !errors.username && !errors.password;

  return (
    <form className="login-card" onSubmit={handleSubmit}>
      <h2>登录</h2>
      <div className="form-item">
        <label>用户名</label>
        <input type="text" name="username"
          value={form.username} onChange={handleChange} />
        {errors.username && (
          <span className="error">{errors.username}</span>
        )}
      </div>
      <div className="form-item">
        <label>密码</label>
        <input type="text" name="password"
          value={form.password} onChange={handleChange} />
        {errors.password && (
          <span className="error">{errors.password}</span>
        )}
      </div>
      <button type="submit" disabled={!isValid}>登录</button>
    </form>
  );
}

这个登录表单展示了受控组件的几个关键优势:

实时验证 :用户每输入一个字符,validate 函数就会被调用,立即检查输入是否合法。用户名不够3个字符?密码不到6位?错误提示立刻出现在输入框下方。这种即时反馈是受控组件的"杀手级特性"。

动态按钮状态 :登录按钮的 disabled 属性由 isValid 变量控制。只有当用户名和密码都填写了且没有错误时,按钮才会变为可点击状态。按钮的状态与表单数据始终保持同步,不需要写任何额外的逻辑。

集中式数据管理 :所有的表单数据都存储在 form 这个对象中。如果你想在提交时获取表单数据,直接用 form.usernameform.password 就行了,不需要去页面上查找 DOM 元素。

这就是"单一数据源"的优雅之处------你的 UI 始终是状态的忠实反映。


四、非受控组件:让 DOM 自己管自己

4.1 核心思想

非受控组件的哲学是:表单元素自己管理自己的状态,React 只在需要的时候通过 ref 去读取

这更接近传统的 HTML 表单行为------输入框的值存储在 DOM 中,而不是 React 的状态中。你不需要为每一个按键编写事件处理函数,也不需要维护一大堆 state。

4.2 最简单的非受控组件

项目中的 UncontrolledInput 组件完美展示了非受控模式:

jsx 复制代码
import { useRef } from 'react';

function UncontrolledInput() {
  const inputRef = useRef(null);

  const handleClick = () => {
    console.log(inputRef.current.value);
  };

  return (
    <>
      <input type="text" ref={inputRef} />
      <button onClick={handleClick}>获取输入值</button>
    </>
  );
}

这段代码和受控版本的区别在哪里?

  1. 没有 useState:没有状态来追踪输入框的值。
  2. 没有 value 属性:输入框没有被 React 控制,它可以自由地显示用户输入的内容。
  3. 没有 onChange:我们并不关心每一次按键。
  4. 使用 ref={inputRef} :这是非受控的关键。inputRef 像一个"指针",直接指向页面上的 DOM 元素。
  5. 按需读取 :用户点击按钮时,通过 inputRef.current.value 直接读取 DOM 中当前的输入值。

数据流极其简单:用户输入 → 字符显示在输入框中(由浏览器处理)→ 需要时,React 通过 ref 去"看一眼"。

4.3 另一个非受控示例:评论框

CommentBox 组件展示了非受控模式在 textarea 上的应用:

jsx 复制代码
function CommentBox() {
  const textareaRef = useRef();

  const handleSubmit = () => {
    const comment = textareaRef.current.value;
    if (!comment) return;
    console.log(comment);
  };

  return (
    <div>
      <textarea placeholder="输入评论..." ref={textareaRef} />
      <button onClick={handleSubmit}>提交评论</button>
    </div>
  );
}

这里的逻辑和非受控输入框完全一样:textarea 自由接受用户输入,只有在点击"提交评论"按钮时,React 才通过 ref 获取内容。

4.4 为什么叫"非受控"?

"非受控"并不意味着"失控"或"不受管理"。它只是说:这个表单元素的值不被 React 的 state 控制。浏览器(DOM)才是这个值的"所有者",React 只是偶尔去访问它一下。

这就好比一个学生(表单元素)自己管理自己的作业进度(输入值),老师(React)不会每一步都盯着,但会在交作业那天(提交事件)查看结果。


五、受控 vs 非受控:一张表看懂区别

对比维度 受控组件 非受控组件
数据存储位置 React state DOM 节点
值的更新方式 每次 onChange 触发 setState 浏览器自动更新,React 通过 ref 读取
是否需要 state 是(每个输入都对应一个 state)
重新渲染 每次输入都触发重新渲染 不触发(除非手动处理)
实时验证 天然支持 需要额外实现
代码量 相对多一些(需要 state + handler) 相对少一些
适合场景 需要即时反馈的表单(搜索、验证、联动) 简单表单、只提交时读取的场景
React 哲学的契合度 高度契合(声明式、数据驱动) 偏低(命令式、DOM 操作)

六、实战指南:什么时候用哪个?

6.1 优先使用受控组件

React 官方文档推荐在大多数情况下使用受控组件。为什么?因为受控组件更符合 React 的声明式编程范式

回想一下 React 的核心公式:UI = f(state)。你的界面是状态的函数。当状态改变时,界面自动更新。受控组件完美地实现了这一公式------输入框的值就是 state 的直接映射。

具体来说,以下场景你应该选择受控组件:

  • 需要实时验证(如注册表单的用户名长度检查、密码强度指示器)
  • 需要条件渲染(如"密码不匹配"提示仅在某些条件下显示)
  • 需要禁用/启用提交按钮(如表单未填完时按钮灰色)
  • 需要格式化输入(如手机号自动加空格、信用卡号分组)
  • 多个输入之间有联动关系(如选择国家后自动切换区号)

6.2 非受控组件的适用场景

非受控组件并非"过时的"或"不好的"选择。在以下情况下,它可能更合适:

  • 简单的、一次性的表单(如一个搜索框或评论框,只在提交时关心内容)
  • 需要集成非 React 代码(如使用第三方富文本编辑器、文件上传组件)
  • 性能敏感场景(避免每次按键都触发整个组件树的重新渲染)
  • 快速原型开发(不想写太多样板代码的时候)

记住一句话:选择取决于你需不需要在每次输入变化时做些什么。如果需要,用受控;如果不需要,非受控可以让你少写很多代码。


七、深层理解:两种范式背后的设计哲学

7.1 声明式 vs 命令式

受控组件和非受控组件之争,本质上是声明式编程命令式编程两种思维方式的碰撞。

声明式(受控) :你告诉 React "输入框的值应该是 value",React 负责确保这一点。你描述的是你想要什么(what),而不是怎么做(how)。

jsx 复制代码
// 声明式:"输入框的值应该永远是 name 这个 state"
<input value={name} onChange={handleChange} />

命令式(非受控):你直接操作 DOM,告诉浏览器"去把这个输入框的值拿给我"。你描述的是怎么做(how),而不是你想要什么(what)。

jsx 复制代码
// 命令式:"去给我拿输入框里的值"
const value = inputRef.current.value;

React 的设计哲学整体偏向声明式。这也是为什么受控组件被认为是"更 React 化"的写法------它让数据流变得清晰、可预测。

7.2 类比 Vue 的理解

如果你之前学过 Vue,可能会觉得 React 的非受控组件中的 useRef 和 Vue 的 ref 很像------确实如此!它们都允许你直接访问 DOM 元素。

但有一个重要区别:Vue 的 ref<script setup> 中是响应式的,而 React 的 useRef 不是响应式的 ------修改 ref.current 不会触发重新渲染。这是很多从 Vue 转 React 的开发者容易踩的坑。

另外,Vue 有 v-model 这个语法糖,它本质上就是一种"受控"模式------输入框的值和 data 中的属性双向绑定。React 没有这样的语法糖,你需要手动写 value + onChange,这显得更啰嗦,但也让数据流更加显式单向


八、React 19 与本文的关系

本文的示例代码使用的是 React 19("react": "^19.2.6")。React 19 在表单处理方面带来了一些改进,但与本文讨论的"受控 vs 非受控"核心概念完全兼容。受控与非受控的范式在 React 19 中依然有效,你不需要担心版本升级导致这些知识过时。

事实上,React 团队在 React 19 中引入了 useActionStateuseFormStatus 等新的 Hook,它们为表单处理提供了更高级的抽象,但这些新特性的底层,依然是受控与非受控思想的延伸。


九、常见误区与注意事项

误区一:非受控 = 不安全的

很多初学者听到"非受控"这个词,会天然地觉得它不好。实际上,"受控"和"非受控"只是两种策略,没有绝对的好坏之分。React 官方文档明确说:"在大多数情况下,我们推荐使用受控组件......但非受控组件也是一种有效的选择。"

误区二:受控组件一定要为每个字段写一个 state

看看项目中的 LoginForm------它使用了一个对象 { username: "", password: "" } 来管理整个表单,而不是为每个字段分别创建 state。这是一种实用的模式,避免了 state 数量的膨胀。

误区三:使用非受控组件就不能做验证了

你可以做验证,只是时机不同。受控组件在每次按键时验证,非受控组件可以在提交时一次性验证。选择哪种验证时机,取决于你的产品需求。

注意:受控组件中 value 为 undefined 或 null

如果你给受控组件的 value 设置了 undefinednull,React 会认为你"放弃控制",输入框会变成非受控状态。这在 React 中会产生一个警告。所以,如果你要使用受控组件,确保 value 始终是一个有效的字符串(哪怕是空字符串 "")。


十、总结

让我们回顾一下本文的核心要点:

  1. 受控组件 的表单值存储在 React state 中,通过 value + onChange 实现数据绑定,适合需要实时响应用户输入的场景。
  2. 非受控组件 的表单值存储在 DOM 中,通过 useRef 在需要时读取,适合简单表单或提交时一次性处理的场景。
  3. 受控组件更符合 React 的声明式哲学(UI = f(state)),也是官方推荐的主要方式。
  4. 两者没有绝对的好坏,关键是根据实际场景做出合理选择。
相关推荐
触底反弹1 小时前
别再 Prop Drilling 了!一文彻底搞懂 React 组件通信的 5 种方案
前端·javascript·react.js
何时梦醒1 小时前
React 进阶必修:彻底搞懂受控组件与非受控组件
前端·javascript·react.js
lllsure2 小时前
Vue&React Router
前端·vue.js·react.js
sugar__salt2 小时前
深入理解 React useContext:跨层级组件通信的核心利器
前端·javascript·react.js·前端框架·框架
用户9385156350716 小时前
React 受控/非受控组件与 React.memo 性能优化——从本质到实战
前端·javascript·react.js
半个落月17 小时前
从零梳理 React Router:路由、懒加载、嵌套页面与登录鉴权
前端·react.js
光影少年18 小时前
Codex 斜杠指令体系与业务场景调度实战
前端·react.js·reactivex
Darling噜啦啦20 小时前
别再逐层传 Props 了!useContext + 自定义 Hook 彻底搞定 React 跨层通信
react.js
先吃饱再说20 小时前
层层传递太“痛”了:useContext 给 React 组件装了个“无线遥控器”
前端·react.js·前端框架