React 受控组件与非受控组件完全指南

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 属性绑定到 value state,任何输入都会触发 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,当用户点击提交按钮时,浏览器会:

  1. 收集表单数据(根据 name 属性)。
  2. 发送请求到表单的 action 地址(默认为当前页面的 URL)。
  3. 重新加载页面(或跳转),导致整个页面的状态丢失。

在 React 单页应用(SPA)中,我们希望用 JavaScript 异步发送数据(AJAX/Fetch),在不刷新页面的情况下更新界面。因此,必须阻止默认行为,将控制权交给我们的代码。


六、总结

  1. 受控组件由 React state 驱动,数据与视图完全同步,适合需要实时交互和验证的复杂表单。它是 React 中推荐的表单处理方式。
  2. 非受控组件由 DOM 自身维护,通过 ref 获取值,代码简洁,适用于简单场景或必须直接操作 DOM 的情况(如文件上传)。
  3. 在实际开发中,优先考虑受控组件,当遇到不需要实时响应、仅在提交时读取值的场景时,再考虑非受控组件。
  4. 不要忘记在表单提交事件中调用 e.preventDefault(),以确保我们能够用 JavaScript 完全控制表单提交过程。

希望通过本文的讲解,你能对 React 中的受控与非受控组件有更清晰的认识,并能在实际项目中灵活运用。如果你有任何疑问或更好的见解,欢迎在评论区留言讨论!

相关推荐
不会敲代码12 小时前
React Hooks 进阶:useRef 核心用法与受控/非受控组件实战解析
前端·react.js·面试
恋猫de小郭2 小时前
Android 官方正式官宣 AI 支持 AppFunctions ,Android 官方 MCP 和系统级 OpenClaw 雏形
android·前端·flutter
孟陬2 小时前
Tanstack Start 的天才创新之处——基于实际使用体验
react.js·visual studio code·next.js
Moment2 小时前
一周重写 Next.js?Cloudflare 和 AI 做到了😍😍😍
前端·javascript·后端
CodeSheep3 小时前
同事去年绩效是C,提离职领导死活不让走,后来领导私下说他走了,就没人背这个绩效了
前端·后端·程序员
摸鱼的春哥3 小时前
春哥的Agent通关秘籍12:本地RAG实战(中下)向量化与落库
前端·javascript·后端
明月_清风3 小时前
毫秒级响应:前端本地搜索的“降维打击”
前端·indexeddb
摸鱼的春哥3 小时前
专家实验让AI做战争决策,AI的选择太暴力了
前端·javascript·后端
明月_清风3 小时前
存储配额:用 navigator.storage.estimate() 预判浏览器什么时候会删你的数据
前端·indexeddb