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.username 和 form.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 中引入了 useActionState 和 useFormStatus 等新的 Hook,它们为表单处理提供了更高级的抽象,但这些新特性的底层,依然是受控与非受控思想的延伸。


九、常见误区与注意事项

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

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

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

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

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

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

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

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


十、总结

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

  1. 受控组件 的表单值存储在 React state 中,通过 value + onChange 实现数据绑定,适合需要实时响应用户输入的场景。
  2. 非受控组件 的表单值存储在 DOM 中,通过 useRef 在需要时读取,适合简单表单或提交时一次性处理的场景。
  3. 受控组件更符合 React 的声明式哲学(UI = f(state)),也是官方推荐的主要方式。
  4. 两者没有绝对的好坏,关键是根据实际场景做出合理选择。
相关推荐
kill52216 小时前
useRef
前端·javascript·react.js
西柚小萌新17 小时前
【LLM&&AI应用开发 八股文】--4.2.Agent智能体(中)
前端·javascript·react.js
Java后端的Ai之路18 小时前
03_React_JSX
前端·react.js·前端框架
晴天小庭2 天前
Sael——基于Jev的AI中转站的安全风控网关,现已开源
人工智能·后端·react.js
Java后端的Ai之路2 天前
01-React基础教程
开发语言·前端·python·react.js·前端框架
流水白开2 天前
React的Virtual DOM、Diff算法和Fiber
前端·react.js
光影少年2 天前
如果 React 组件的属性没有传值,它的默认值是什么?
前端·javascript·react.js
Fate_I_C2 天前
Capacitor 应用鸿蒙化实战:用 hionic 把 React 应用跑在 OpenHarmony 上
react.js·华为·harmonyos
liangshanbo12152 天前
面试题:React.memo 和 useMemo 的区别
前端·react.js·前端框架