【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,它们在底层是如何处理受控与非受控的?你觉得它们更偏向于哪种模式,为什么?在评论区分享你的研究和看法,我们一起探讨!

相关推荐
l_tian_tian_1 分钟前
SpringClound——网关、服务保护和分布式事务
linux·服务器·前端
一只小风华~25 分钟前
CSS @media 媒体查询
前端·css·媒体
shix .1 小时前
最近 | 黄淮教务 | 小工具合集
前端·javascript
John_ToDebug1 小时前
Chrome 内置扩展 vs WebUI:浏览器内核开发中的选择与实践
前端·c++·chrome
烛阴2 小时前
解锁动态键:TypeScript 索引签名完全指南
前端·javascript·typescript
上单带刀不带妹2 小时前
ES6 中的 Proxy 全面讲解
前端·ecmascript·es6·proxy
11054654013 小时前
37、需求预测与库存优化 (快消品) - /供应链管理组件/fmcg-inventory-optimization
前端·信息可视化·数据分析·js
nunumaymax4 小时前
在图片没有加载完成时设置默认图片
前端
你怎么知道我是队长4 小时前
C语言---编译的最小单位---令牌(Token)
java·c语言·前端