第四章: 跨组件通信与逻辑复用
第四章: 跨组件通信与逻辑复用
Prorise第四章: 跨组件通信与逻辑复用
摘要: 在前几章,我们掌握了通过 Props 进行父子通信,以及在组件内部管理状态和副作用的核心能力。然而,当应用变得复杂,跨越多个层级的“远距离”通信和在组件间共享相似的逻辑就成了新的挑战。本章将直面这两个痛点,首先引入 Context
机制,彻底解决“属性钻探”问题;接着,我们将学习 useRef
,掌握在 React 中与 DOM 交互及存储持久化变量的能力;最后,我们将所有知识融会贯通,学习 React 最强大的模式——自定义 Hooks,将组件逻辑提升到前所未有的可复用高度。
在本章中,我们将沿着一条清晰的“问题-解决方案”路径,解锁 React 的高级能力:
- 首先,我们将重新审视在
2.2.3
节提出的 “属性钻探” (Prop Drilling) 问题。 - 接着,我们将引入 React 官方的解决方案 Context API 与
useContext
Hook,学习如何在组件树中进行“大范围”的状态共享,这精确对标 Vue 的provide/inject
。 - 然后,我们将解决另一个常见需求:如何在 React 中直接操作 DOM 元素。我们将学习
useRef
Hook 来应对这类场景,它对标 Vue 的模板引用 (template refs)
。 - 最后,也是本章的最高潮,我们将学习如何将前面学到的所有 Hooks 组合起来,创建属于我们自己的 自定义 Hooks (Custom Hooks),实现优雅、彻底的逻辑复用。
4.1. 解决属性钻探:Context API 与 useContext
本小节核心知识点:
Context
提供了一种在组件树中共享“全局”数据的方式,而无需手动地在每一层组件中传递 props。- 它精确对标 Vue 的 provide 和 inject
React.createContext()
: 用于创建一个 Context 对象。<MyContext.Provider value={...}>
: Provider 组件,用于“提供”数据。它包裹的任何子组件都能访问到这个value
。useContext(MyContext)
: Hook,用于在子组件中“注入”并读取 Provider 提供的value
。
4.1.1. 痛点重现:语义化场景下的属性钻探
我们在 2.2.3
节已经理论上了解了属性钻探。现在,让我们在一个真实场景中感受它的痛苦。假设我们有以下组件结构,App
组件获取了当前登录的用户信息,但只有最深层的 Greeting
组件需要显示用户名。
1 | # src/ |
App.tsx (数据源)
1 | import UserProfilePage from './components/UserProfilePage'; |
UserProfilePage.tsx (中间人 A)
1 | import UserInfoCard from './UserInfoCard'; |
UserInfoCard.tsx (中间人 B)
1 | import Greeting from './Greeting' |
Greeting.tsx (最终消费者)
1 | // 只有 Greeting 组件真正使用了 user.name |
这种层层传递让中间组件变得臃肿且高度耦合,维护起来就是一场噩梦。
4.1.2. 解决方案:三步构建 Context
现在,我们用 Context 来彻底重构这个流程。
第一步:创建 Context 对象
最佳实践是为你的 Context 创建一个单独的文件。
文件路径: src/contexts/UserContext.ts
(新建)
1 | import { createContext } from 'react'; |
第二步:在顶层提供 (Provide) Context
回到我们的 App.tsx
,使用 <UserContext.Provider>
来包裹整个应用,并通过 value
prop 将数据“注入”到组件树中。
文件路径: src/App.tsx
(修改)
1 | import { useState } from 'react'; |
现在,被 Provider
包裹的所有后代组件,无论嵌套多深,都具备了直接访问 currentUser
数据的能力。
第三步:在深层组件中消费 (Consume) Context
这是最激动人心的一步。我们现在可以直接在 Greeting
组件中获取数据,而完全绕过中间组件。
文件路径: src/components/Greeting.tsx
(修改)
1 | import { useContext } from 'react'; |
useContext
是迄今为止最简洁、最直观的消费 Context 的方式。
文件路径: src/components/Greeting.tsx
(旧版写法,仅作了解)
1 | import { UserContext } from '../contexts/UserContext' |
<Context.Consumer>
是一种基于 Render Props 模式的旧方法。当需要消费多个 Context 时,它会导致多层嵌套(俗称“回调地狱”),可读性很差。在现代 React 开发中,应 始终优先使用 useContext
Hook。
最终成果:解耦的中间组件
现在,我们的中间组件 UserProfilePage
和 UserInfoCard
不再需要关心 user
prop,它们变得干净、独立且高度可复用。
UserProfilePage.tsx (重构后)
1 | import UserInfoCard from './UserInfoCard'; |
Context 总结:Context
是解决 React 中“跨级组件通信”问题的官方标准答案。它允许我们将一些“全局性”的数据(如用户身份、主题、语言设置等)从顶层注入,让任何深度的子组件都能按需、直接地获取,从而实现组件间的彻底解耦。
4.2. 引用与命令式操作:useRef
的双重角色
在 React 的声明式世界里,我们通常不直接操作 DOM。但总有一些场景,我们必须“命令式地”与 DOM 元素交互,比如让一个输入框聚焦。useRef
就是 React 为这类场景提供的官方“后门”。
本小节核心知识点:
useRef
返回一个可变的 ref 对象,其.current
属性被初始化为您传入的参数 (useRef(initialValue)
)。useRef
有两大核心用途:- 访问 DOM 节点,这精确对标 Vue 的
模板引用
。 - 存储一个不触发组件重新渲染的可变值,类似于 Vue 3
script setup
中一个普通的、非响应式的变量。
- 访问 DOM 节点,这精确对标 Vue 的
- 改变
ref.current
的值 不会 引起组件的重新渲染。这是它与useState
的根本区别。
4.2.1. 核心用途一:访问 DOM 元素
痛点背景: 在 Vue 中,我们可以通过给元素添加 ref="myInput"
属性,然后在 <script setup>
中通过 const myInput = ref(null)
来获取该 DOM 元素的引用,并调用它的方法,如 myInput.value.focus()
。React 如何实现同样的功能?
范式转变:从模板字符串到 Ref 对象
React 的实现方式思想一致,但语法上更贴近 JavaScript 的对象引用。总共分三步:创建 Ref -> 附加 Ref -> 访问 Ref。
让我们通过一个“点击按钮聚焦输入框”的经典案例来掌握它。
文件路径: src/components/FocusableInput.tsx
1 | import { useRef } from "react"; |
4.2.2. 核心用途二:存储持久化的可变值
痛点背景: 假设我们需要在一个组件中设置一个 setInterval
定时器。我们需要在某处存储这个定时器的 ID,以便在组件卸载或用户点击“停止”按钮时能够调用 clearInterval(timerId)
来清除它。
如果我们用 useState
来存储 timerId
,会发生什么?const [timerId, setTimerId] = useState(null)
。setInterval
返回 ID 后调用 setTimerId(id)
会导致组件不必要地重新渲染。我们只是想存个值,并不想因为这个值的改变而刷新界面。
解决方案:将 useRef
用作“实例变量”
useRef
完美地解决了这个问题。它创建的 ref 对象在组件的整个生命周期内都是同一个对象。我们可以把需要持久化,但又与渲染无关的值,存放在它的 .current
属性中。
文件路径: src/components/IntervalTimer.tsx
1 | import { useRef, useEffect, useState } from "react"; |
在这个例子中,intervalRef
就像一个忠诚的管家,它默默地为我们保管着 intervalId
,无论组件因为 count
的变化重新渲染多少次,它都稳定地持有那个值,直到我们需要用它为止。useState
vs. useRef
何时使用?
- 当你希望值的改变能够触发界面更新时 -> 使用
useState
。这是驱动 React 声明式 UI 的核心。 - 当你需要访问 DOM 元素,或者需要一个值在多次渲染之间保持不变,但又不希望它的改变触发渲染时 -> 使用
useRef
。
4.3. 逻辑复用的最佳实践:自定义 Hooks
到目前为止,我们已经掌握了 React 提供的所有基础 Hooks。但 React 最强大的地方在于,它允许我们将这些基础工具组合起来,创造出属于我们自己的、可复用的逻辑单元——这就是自定义 Hooks (Custom Hooks)。
本小节核心知识点:
- 自定义 Hook 是一个以
use
开头的 JavaScript 函数,其内部可以调用其他的 Hooks (如useState
,useEffect
)。 - 它是 React 中 实现状态逻辑复用 的首选方式,完美替代了旧有的 HOC 和 Render Props 模式。
- 自定义 Hook 使得我们将组件中与 UI 无关的逻辑(如:数据请求、事件监听、表单处理)抽离成独立的、可测试的、可在多个组件间共享的单元。
4.3.1. 痛点背景:当组件逻辑开始重复
想象一下,我们应用中的很多组件都需要从 API 获取数据。按照我们已有的知识,每个组件可能都会包含类似下面这样的代码:
1 | import { useState, useEffect } from "react"; |
现在,如果另一个组件 CommentList
也需要获取评论数据,我们就得把上面这一大段 useState
和 useEffect
的逻辑原封不动地复制粘贴过去,只改一下 URL。这显然违反了 DRY (Don’t Repeat Yourself) 原则,难以维护。
4.3.2. 解决方案:创建你的第一个自定义 Hook
自定义 Hook 就是为了解决这类问题而生的。它让我们能将这部分可复用的 状态逻辑 封装到一个函数中。
自定义 Hook 的两大黄金法则:
- 必须以
use
开头: 这是 React Linter 用来识别一个函数是否为 Hook 的硬性规定,例如useFetch
、useToggle
。 - 内部可以调用其他 Hooks: 这是自定义 Hook 的超能力所在,它能组合
useState
、useEffect
等,创造出新的、更强大的 Hook。
现在,让我们根据以下三个示例,由简到繁,一步步构建并使用三个实用的自定义 Hook。
示例一:useToggle
- 封装最简单的状态切换
这是自定义 Hook 的 “Hello World”。它封装了一个常见的布尔值切换逻辑。
文件路径: src/hooks/useToggle.ts
1 | import { useState } from "react"; |
文件路径: src/components/ToggleComponent.tsx
1 | import useToggle from '../hooks/useToggle' |
示例二:useInput
- 简化表单处理
这个 Hook 封装了处理受控输入框 value
和 onChange
的通用逻辑。
文件路径: src/hooks/useInput.ts
1 | import { useState } from 'react' |
当你写 {…name} 时,实际上是在展开 useInput Hook 返回的对象。让我们看看这个过程:
1 | // useInput 返回的对象 |
文件路径: src/components/FormComponent.tsx
1 | import useInput from '../hooks/useInput' |
示例三:useFetch
- 封装异步数据获取(最强示例)
这正是我们最初那个痛点的完美解决方案。它封装了数据、加载状态、错误状态以及数据获取的整个 useEffect
逻辑。
文件路径: src/hooks/useFetch.js
1 | import { useEffect, useState } from 'react' |
文件路径: src/components/FetchComponent.jsx
1 | import useFetch from '../hooks/useFetch' |
4.3.3. 组合与应用
最后,我们可以在 App.jsx
中轻松地将这些由自定义 Hook 驱动的组件组合在一起,每个组件都只关心自己的 UI 呈现,而将复杂的逻辑“外包”给了 Hooks。
文件路径: src/App.jsx
1 | import FetchComponent from "./components/FetchComponent"; |
自定义 Hooks 总结:
自定义 Hooks 是 React 逻辑复用的基石。通过将有状态的逻辑从组件中抽离出来,我们获得了:
- 高度的可复用性:
useFetch
可以在任何需要获取数据的组件中使用。 - 清晰的关注点分离: 组件可以专注于“做什么”(渲染 UI),而 Hook 则负责“怎么做”(状态管理的细节)。
- 更强的可读性和可维护性: 组件代码变得极其简洁和声明式。
- 独立的可测试性: 我们可以脱离 UI,单独对自定义 Hook 的逻辑进行单元测试。
掌握自定义 Hooks,是真正从 React “使用者”迈向“精通者”的关键一步。