【React 设计模式】受控与非受控:解构 React 组件设计的核心模式

【React 设计模式】受控与非受控:解构 React 组件设计的核心模式

所属专栏: 《前端小技巧集合:让你的代码更优雅高效》
上一篇: 【React Key】揭秘 React 的"身份证":深入理解 key 的重要性与最佳实践
作者: 码力无边


引言:你的表单,究竟是谁的"傀儡"?

嘿,各位在 React 世界里构筑交互体验的道友们,我是码力无边

在我们的前端江湖中,表单 (<form>) 就像是一座连接用户与应用的"桥梁"。用户通过这座桥梁输入信息,应用则接收这些信息并作出响应。但在 React 的世界里,这座"桥梁"的建造方式,却有两种截然不同的"流派":

  • 受控组件 (Controlled Components)
  • 非受控组件 (Uncontrolled Components)

这两种模式的核心区别在于一个哲学问题:表单中的数据(比如 <input>value),它的"唯一事实来源 (Single Source of Truth)"究竟在哪里?

  • 受控组件流派 认为:所有的数据都应该由 React 的 state 来"控制"。真实的 DOM 节点(如 <input>) 只不过是一个"傀儡",它的显示内容完全由 React state 决定。
  • 非受控组件流派 则认为:表单数据应该回归传统,由真实的 DOM 节点自己来"做主"。React 只是在需要的时候去 DOM 那里"读取"一下数据,平时并不干涉。

这两种流派没有绝对的优劣之分,它们是 React 为我们提供的、适用于不同场景的两种设计模式。然而,React 官方强烈推荐在大多数情况下使用受控组件,因为它能让组件的行为变得更可预测、更易于管理。

但很多开发者,尤其是在从 jQuery 或传统前端转向 React 时,常常会混淆这两种模式,或者不理解为什么需要"受控"。

今天,码力无边就将带你深入这两种模式的内核,解构它们的运作机制、优缺点以及各自的最佳实战场景。掌握它们,你就能根据实际需求,选择最合适的"流派"来建造你的"数据之桥",让你的表单交互变得既强大又优雅。

一、受控组件:React state 的"绝对统治"

在受控组件模式下,React 组件的 state 是表单数据的唯一和最终的"权威"。我们通过 propsstate 的值传递给表单元素(如 inputvalue),并通过 onChange 回调函数来更新这个 state

一个最经典的受控 <input>

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

function ControlledInput() {
  const [name, setName] = useState(''); // 1. state 是唯一事实来源

  const handleChange = (event) => {
    // 3. onChange 回调负责更新 state
    setName(event.target.value); 
  };

  return (
    <>
      <p>当前输入: {name}</p>
      <input 
        type="text" 
        value={name} // 2. input 的显示内容完全由 state 控制
        onChange={handleChange} 
      />
    </>
  );
}

让我们来"慢动作"解析一下用户输入一个字母 a 的过程:

  1. 用户在键盘上按下 a
  2. <input> 元素的 onChange 事件被触发,handleChange 函数被调用。
  3. handleChange 调用 setName('a'),请求更新 React 的 state
  4. React 响应状态更新,重新渲染 ControlledInput 组件。
  5. 在重新渲染时,<input>value 属性被设置为最新的 state 值,也就是 'a'
  6. 最终,用户在屏幕上看到了字母 a

看到了吗? 用户的输入并没有直接改变 <input>。它只是触发了一个"请求",这个请求被 React 捕获,React 更新了自己的 state,然后反过来"命令"<input> 显示新的内容。<input> 成了 state 的一个"提线木偶",完全受其控制。

受控组件的"神权"优势:
  1. 可预测性与即时验证

    由于 state 是唯一数据源,我们可以在 onChange 事件中对用户的输入进行即时的验证、格式化或限制

    jsx 复制代码
    const handleChange = (event) => {
      // 只允许输入数字
      const value = event.target.value.replace(/\D/g, '');
      setPinCode(value);
    };
    
    // 或者,限制输入长度
    const handleUsernameChange = (event) => {
      if (event.target.value.length <= 10) {
        setUsername(event.target.value);
      }
    }
  2. 动态禁用提交按钮

    我们可以轻易地根据多个表单字段的状态,来决定提交按钮是否可用。

    jsx 复制代码
    const isSubmitDisabled = username.trim() === '' || password.length < 6;
    <button type="submit" disabled={isSubmitDisabled}>提交</button>
  3. 单一状态管理

    整个表单的状态都集中在组件的 state 中,使得调试、重置表单、或者将表单数据传递给其他组件变得非常简单。

受控组件的"代价":
  1. 更多的代码 :每个表单元素都需要一个对应的 state 和一个 onChange 处理函数,对于大型表单来说,这可能会导致代码变得冗长。
  2. 性能考量 :每一次按键都会触发一次 state 更新和组件的重新渲染。对于绝大多数表单,这点性能开销可以忽略不计。但对于一些需要极高性能的输入场景(比如实时协作编辑器),这种高频次的 re-render 可能需要额外的优化。

二、非受控组件:DOM 的"自我救赎"

在非受控组件模式下,我们放弃了对表单数据的"绝对统治"。数据直接由 DOM 节点自己存储和管理 。React 组件不控制它的值,只在需要时通过 ref 去"读取"它。

一个典型的非受控 <input>

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

function UncontrolledInput() {
  // 1. 创建一个 ref 来直接访问 DOM 节点
  const inputRef = useRef(null);

  const handleSubmit = (event) => {
    event.preventDefault();
    // 2. 在需要时,通过 ref.current.value 读取 DOM 的当前值
    alert(`A name was submitted: ${inputRef.current.value}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input 
          type="text" 
          // 3. 使用 defaultValue 来设置初始值,而非 value
          defaultValue="Guest" 
          ref={inputRef} 
        />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

运作机制解析:

  • 我们不再使用 useState 来管理输入框的值。
  • 我们使用 useRef 创建了一个 ref,并将其附加到 <input> 元素上。
  • 我们用 defaultValue 属性来设置初始值。一旦渲染完成,这个 defaultValue 就和 React "无关"了,用户可以自由地在输入框中输入,这个过程不会触发任何 React 的 re-render
  • 只有当 onSubmit 事件触发时,我们才通过 inputRef.current.value 去"询问"DOM 节点:"嘿,你现在的值是什么?"
非受控组件的"自由"优势:
  1. 代码更简洁:对于简单的表单,尤其是那些"一次性"获取数据的场景,非受控组件的代码量更少。
  2. 性能更高:由于用户的输入不会引起组件的重新渲染,它在性能上几乎没有开销。
  3. 易于与第三方 DOM 库集成 :当你需要使用一些直接操作 DOM 的库(如某些 D3.js 图表或 jQuery 插件)时,非受控组件和 ref 是你的不二之选。
  4. 文件上传 <input type="file" /> :文件输入框的值是只读的,你只能通过 ref 来访问它,因此它天生就是一个非受控组件
非受控组件的"混乱"代价:
  1. 数据获取不及时 :你只能在特定事件(如 submit)触发后才能获取到数据,无法做到实时验证或格式化。
  2. 难以实现复杂交互:像上面提到的"动态禁用提交按钮"这类依赖于表单实时状态的交互,用非受控组件实现起来会非常困难和"不 React"。
  3. 状态分散 :"唯一事实来源"分散在各个 DOM 节点中,而不是集中在 React 的 state 里,这违背了 React 的核心思想,也让状态管理变得复杂。

三、受控 vs 非受控:抉择的艺术

特性 受控组件 (Controlled) 非受控组件 (Uncontrolled)
数据源 (SSoT) React state DOM 自身
数据流 state -> value, onChange -> setState (双向绑定) defaultValue -> DOM, ref -> 读取 DOM (单向读取)
实时性 ,每次输入都更新 state ,仅在需要时通过 ref 读取
代码量 相对较多 相对较少
性能 每次输入都 re-render 无输入 re-render
推荐场景 绝大多数表单场景,需要实时验证/格式化、动态交互、统一状态管理的场景 简单的、一次性的数据提交、与第三方 DOM 库集成、文件上传
一句话总结 React 全权管理 DOM 自治,React 仅读取

一个混合使用的例子:

在大型复杂表单中,你完全可以混合使用这两种模式。比如,一个个人信息表单,usernameemail 字段需要实时验证,使用受控模式 ;而一个"个人简介"的 <textarea>,用户可能会输入大段文字,为了避免不必要的 re-render,可以暂时使用非受控模式 ,只在提交时通过 ref 读取其内容。

写在最后:拥抱"受控",但不惧"放权"

在 React 的世界里,受控组件是"王道"。它让你的 UI 状态与 React 的数据流保持了完美同步,使得组件的行为变得高度可预测和可控。这是构建复杂、健壮的 React 应用的基石。在绝大多数情况下,你都应该优先选择受控组件。

然而,非受控组件并非"异端"。它是在特定场景下,为了追求简洁、性能或与外部世界(DOM 库)和谐共存而存在的"明智的妥协"。它提醒我们,React 并没有完全切断我们与底层 DOM 的联系,ref 就是那把能让我们在必要时"放权"给 DOM 的钥匙。

真正的高手,不是固守于某一"流派",而是能深刻理解两种模式的本质,并根据战场(业务需求)的变化,灵活地选择最合适的兵器。


专栏预告与互动:

我们已经掌握了 React 组件设计的核心模式。但 ref 的能力,远不止获取 DOM 节点那么简单。你知道 useRef 还能用来存储任何可变值,且不会触发 re-render 吗?你知道如何通过 useImperativeHandle 让子组件向父组件"暴露"特定的方法吗?

下一篇,我们将深入探讨 useRef 的"隐藏"用途,并揭秘 useImperativeHandle 这个略显神秘的 Hook,让你对 ref 的理解提升到一个新的维度!

感觉码力无边的"设计模式"解构让你对组件的理解更上一层楼?别忘了点赞、收藏、关注,你的每一次支持,都是我深入 React 核心、分享更多真知的强大动力!

今日论道: 很多流行的表单库,如 Formik 或 React Hook Form,它们在底层是如何处理受控与非受控的?你觉得它们更偏向于哪种模式,为什么?在评论区分享你的研究和看法,我们一起探讨!

相关推荐
xw517 分钟前
npm几个实用命令
前端·npm
!win !21 分钟前
npm几个实用命令
前端·npm
代码狂想家26 分钟前
使用openEuler从零构建用户管理系统Web应用平台
前端
MobotStone1 小时前
为什么第一性原理思维可以改变你解决问题的方式
架构·前端框架
dorisrv2 小时前
优雅的React表单状态管理
前端
蓝瑟2 小时前
告别重复造轮子!业务组件多场景复用实战指南
前端·javascript·设计模式
dorisrv2 小时前
高性能的懒加载与无限滚动实现
前端
韭菜炒大葱3 小时前
别等了!用 Vue 3 让 AI 边想边说,字字蹦到你脸上
前端·vue.js·aigc
StarkCoder3 小时前
求求你,别在 Swift 协程开头写 guard let self = self 了!
前端
清妍_3 小时前
一文详解 Taro / 小程序 IntersectionObserver 参数
前端