【React 设计模式】受控与非受控:解构 React 组件设计的核心模式
所属专栏: 《前端小技巧集合:让你的代码更优雅高效》
上一篇: 【React Key】揭秘 React 的"身份证":深入理解 key
的重要性与最佳实践
作者: 码力无边
✨ 引言:你的表单,究竟是谁的"傀儡"?
嘿,各位在 React 世界里构筑交互体验的道友们,我是码力无边!
在我们的前端江湖中,表单 (<form>
) 就像是一座连接用户与应用的"桥梁"。用户通过这座桥梁输入信息,应用则接收这些信息并作出响应。但在 React 的世界里,这座"桥梁"的建造方式,却有两种截然不同的"流派":
- 受控组件 (Controlled Components)
- 非受控组件 (Uncontrolled Components)
这两种模式的核心区别在于一个哲学问题:表单中的数据(比如 <input>
的 value
),它的"唯一事实来源 (Single Source of Truth)"究竟在哪里?
- 受控组件流派 认为:所有的数据都应该由 React 的
state
来"控制"。真实的 DOM 节点(如<input>
) 只不过是一个"傀儡",它的显示内容完全由 Reactstate
决定。 - 非受控组件流派 则认为:表单数据应该回归传统,由真实的 DOM 节点自己来"做主"。React 只是在需要的时候去 DOM 那里"读取"一下数据,平时并不干涉。
这两种流派没有绝对的优劣之分,它们是 React 为我们提供的、适用于不同场景的两种设计模式。然而,React 官方强烈推荐在大多数情况下使用受控组件,因为它能让组件的行为变得更可预测、更易于管理。
但很多开发者,尤其是在从 jQuery 或传统前端转向 React 时,常常会混淆这两种模式,或者不理解为什么需要"受控"。
今天,码力无边就将带你深入这两种模式的内核,解构它们的运作机制、优缺点以及各自的最佳实战场景。掌握它们,你就能根据实际需求,选择最合适的"流派"来建造你的"数据之桥",让你的表单交互变得既强大又优雅。
一、受控组件:React state
的"绝对统治"
在受控组件模式下,React 组件的 state
是表单数据的唯一和最终的"权威"。我们通过 props
将 state
的值传递给表单元素(如 input
的 value
),并通过 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
的过程:
- 用户在键盘上按下
a
。 <input>
元素的onChange
事件被触发,handleChange
函数被调用。handleChange
调用setName('a')
,请求更新 React 的state
。- React 响应状态更新,重新渲染
ControlledInput
组件。 - 在重新渲染时,
<input>
的value
属性被设置为最新的state
值,也就是'a'
。 - 最终,用户在屏幕上看到了字母
a
。
看到了吗? 用户的输入并没有直接改变 <input>
。它只是触发了一个"请求",这个请求被 React 捕获,React 更新了自己的 state
,然后反过来"命令"<input>
显示新的内容。<input>
成了 state
的一个"提线木偶",完全受其控制。
受控组件的"神权"优势:
-
可预测性与即时验证 :
由于
state
是唯一数据源,我们可以在onChange
事件中对用户的输入进行即时的验证、格式化或限制。jsxconst 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); } }
-
动态禁用提交按钮 :
我们可以轻易地根据多个表单字段的状态,来决定提交按钮是否可用。
jsxconst isSubmitDisabled = username.trim() === '' || password.length < 6; <button type="submit" disabled={isSubmitDisabled}>提交</button>
-
单一状态管理 :
整个表单的状态都集中在组件的
state
中,使得调试、重置表单、或者将表单数据传递给其他组件变得非常简单。
受控组件的"代价":
- 更多的代码 :每个表单元素都需要一个对应的
state
和一个onChange
处理函数,对于大型表单来说,这可能会导致代码变得冗长。 - 性能考量 :每一次按键都会触发一次
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 节点:"嘿,你现在的值是什么?"
非受控组件的"自由"优势:
- 代码更简洁:对于简单的表单,尤其是那些"一次性"获取数据的场景,非受控组件的代码量更少。
- 性能更高:由于用户的输入不会引起组件的重新渲染,它在性能上几乎没有开销。
- 易于与第三方 DOM 库集成 :当你需要使用一些直接操作 DOM 的库(如某些 D3.js 图表或 jQuery 插件)时,非受控组件和
ref
是你的不二之选。 - 文件上传
<input type="file" />
:文件输入框的值是只读的,你只能通过ref
来访问它,因此它天生就是一个非受控组件。
非受控组件的"混乱"代价:
- 数据获取不及时 :你只能在特定事件(如
submit
)触发后才能获取到数据,无法做到实时验证或格式化。 - 难以实现复杂交互:像上面提到的"动态禁用提交按钮"这类依赖于表单实时状态的交互,用非受控组件实现起来会非常困难和"不 React"。
- 状态分散 :"唯一事实来源"分散在各个 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 仅读取 |
一个混合使用的例子:
在大型复杂表单中,你完全可以混合使用这两种模式。比如,一个个人信息表单,username
和 email
字段需要实时验证,使用受控模式 ;而一个"个人简介"的 <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,它们在底层是如何处理受控与非受控的?你觉得它们更偏向于哪种模式,为什么?在评论区分享你的研究和看法,我们一起探讨!