v19.2Latest

使用 Context 深层传递数据

通常,你会通过 props 将信息从父组件传递到子组件。但是,如果你必须通过中间的许多组件传递 props,或者你的应用中许多组件需要相同的信息,那么传递 props 可能会变得冗长且不便。Context 允许父组件向其下方的任何组件(无论深度如何)提供一些信息,而无需通过 props 显式传递。

您将学习
  • 什么是“prop drilling”(属性钻取)
  • 如何使用 context 替代重复的 prop 传递
  • context 的常见用例
  • context 的常见替代方案

传递 props 的问题

传递 props是一种很好的方式,可以显式地将数据通过 UI 树传递给使用它的组件。

但是,当你需要将某个 prop 深层传递到树中,或者许多组件需要相同的 prop 时,传递 props 可能会变得冗长且不便。最近的共同祖先可能离需要数据的组件很远,并且将状态提升到那么高的位置可能会导致一种称为“prop drilling”(属性钻取)的情况。

状态提升

包含三个组件树的图表。父组件包含一个代表紫色高亮值的泡泡。该值向下流向两个子组件,两者均为紫色高亮。包含三个组件树的图表。父组件包含一个代表紫色高亮值的泡泡。该值向下流向两个子组件,两者均为紫色高亮。

属性钻取

包含十个节点的树状图,每个节点最多有两个子节点。根节点包含一个代表紫色高亮值的泡泡。该值向下流过两个子节点,每个子节点传递该值但不包含它。左子节点将该值向下传递给两个子节点,这两个子节点均为紫色高亮。根的右子节点将该值传递给其两个子节点中的一个——右侧的子节点,该节点为紫色高亮。该子节点将该值传递给其唯一的子节点,该子节点又将其向下传递给其两个子节点,这两个子节点均为紫色高亮。包含十个节点的树状图,每个节点最多有两个子节点。根节点包含一个代表紫色高亮值的泡泡。该值向下流过两个子节点,每个子节点传递该值但不包含它。左子节点将该值向下传递给两个子节点,这两个子节点均为紫色高亮。根的右子节点将该值传递给其两个子节点中的一个——右侧的子节点,该节点为紫色高亮。该子节点将该值传递给其唯一的子节点,该子节点又将其向下传递给其两个子节点,这两个子节点均为紫色高亮。

如果有一种方法可以“传送”数据到树中需要它的组件,而无需传递 props,那岂不是很好?借助 React 的 context 功能,这可以实现!

Context:传递 props 的替代方案

Context 允许父组件向其下方的整个树提供数据。context 有许多用途。这里有一个例子。考虑这个接受Heading组件,它接受一个表示其大小的level

假设你希望同一个 Section内的多个标题始终具有相同的大小:

目前,您需要将 level属性分别传递给每个<Heading>

如果能将 level属性传递给<Section> 组件,并从 <Heading>中移除它,那就更好了。这样您可以确保同一章节中的所有标题具有相同的大小:

但是 <Heading> 组件如何知道其最近的 <Section>的层级呢?这需要某种方式让子组件能够“询问”树中上方某处的数据。

仅靠属性是无法实现的。这就是上下文发挥作用的地方。您将分三步完成:

  1. 创建一个上下文。(您可以将其命名为LevelContext,因为它是用于标题层级的。)
  2. 在需要数据的组件中使用该上下文。(Heading将使用LevelContext。)
  3. 从指定数据的组件中提供该上下文。(Section 将提供 LevelContext。)

上下文允许父组件——甚至是遥远的父组件!——向其内部的整个树提供一些数据。

在邻近的子组件中使用上下文

包含三个组件树的示意图。父组件包含一个代表值的橙色高亮气泡,该值向下投射到两个子组件,每个子组件都呈橙色高亮。包含三个组件树的示意图。父组件包含一个代表值的橙色高亮气泡,该值向下投射到两个子组件,每个子组件都呈橙色高亮。

在遥远的子组件中使用上下文

包含十个节点的树状图,每个节点最多有两个子节点。根父节点包含一个代表值的橙色高亮气泡。该值直接向下投射到树中的四个叶子节点和一个中间组件,它们都呈橙色高亮。其他中间组件均未高亮。包含十个节点的树状图,每个节点最多有两个子节点。根父节点包含一个代表值的橙色高亮气泡。该值直接向下投射到树中的四个叶子节点和一个中间组件,它们都呈橙色高亮。其他中间组件均未高亮。

步骤 1:创建上下文

首先,您需要创建上下文。您需要从一个文件中导出它,以便您的组件可以使用它:

传递给 createContext的唯一参数是默认值。这里的1指的是最大的标题级别,但你可以传递任何类型的值(甚至是一个对象)。你将在下一步中看到默认值的重要性。

步骤 2:使用上下文

从 React 和你创建的上下文中导入useContext Hook:

目前,Heading组件从 props 中读取level

现在,移除 level prop,并从你刚刚导入的上下文 LevelContext中读取值:

useContext是一个 Hook。就像useStateuseReducer一样,你只能在 React 组件内部(不能在循环或条件语句中)立即调用 Hook。useContext告诉 React,Heading 组件想要读取 LevelContext

既然 Heading 组件没有 levelprop,你就不再需要在 JSX 中像这样向Heading传递 level prop 了:

更新 JSX,让 Section 来接收它:

提醒一下,这是你试图使其正常工作的标记:

注意这个例子目前还不能正常工作!所有标题的大小都一样,因为尽管你使用了上下文,但还没有提供它。React 不知道从哪里获取它!

如果你不提供上下文,React 将使用你在上一步中指定的默认值。在这个例子中,你指定了1作为createContext的参数,所以useContext(LevelContext)返回1,将所有标题都设置为<h1>。让我们通过让每个Section提供自己的上下文来解决这个问题。

步骤 3:提供上下文

当前Section组件渲染其子元素:

用上下文提供者包裹它们,以便向它们提供LevelContext

这告诉 React:“如果这个<Section>内部的任何组件请求LevelContext,就给它们这个level。”组件将使用其上方 UI 树中最近的<LevelContext>的值。

这与原始代码的结果相同,但你不需要将level属性传递给每个Heading组件!相反,它通过询问上方最近的Section来“推断”出它的标题级别:

  1. 你向 level传递一个<Section>属性。
  2. Section将其子组件包裹在<LevelContext value={level}>中。
  3. Heading 通过 LevelContext 使用 useContext(LevelContext)来获取上方最近的LevelContext 值。

在同一组件中使用和提供上下文

目前,你仍然需要手动指定每个 Sectionlevel

由于上下文允许你从上层组件读取信息,每个 Section都可以从上层Section 读取 level,并自动向下传递level + 1。以下是实现方法:

经过此更改,你既不需要向 <Section>传递level属性,也不需要向<Heading>传递该属性:

现在 HeadingSection都通过读取LevelContext 来判断它们的“深度”。并且 Section 将其子组件包裹在 LevelContext中,以指定其内部的任何内容都处于“更深”的层级。

注意

此示例使用标题级别是因为它们直观地展示了嵌套组件如何覆盖上下文。但上下文对于许多其他用例也很有用。你可以向下传递整个子树所需的任何信息:当前主题颜色、当前登录用户等等。

上下文会穿过中间组件

你可以在提供上下文的组件和使用它的组件之间插入任意数量的组件。这包括像 <div>这样的内置组件以及你可能自己构建的组件。

在此示例中,相同的Post组件(带有虚线边框)在两个不同的嵌套层级渲染。请注意,其中的<Heading> 会自动从最近的 <Section>获取其级别:

你无需为此做任何特殊操作。Section为其内部的树指定了上下文,因此你可以在任何位置插入<Heading>,它都会具有正确的大小。在上面的沙盒中试试吧!

上下文让你可以编写“适应其周围环境”的组件,并根据它们在何处(或者换句话说,在哪个上下文中)被渲染来以不同方式显示自身。

上下文的工作原理可能会让你联想到CSS 属性继承。在 CSS 中,你可以为一个color: blue 指定 <div>,那么其内部的任何 DOM 节点,无论嵌套多深,都会继承该颜色,除非中间的某个 DOM 节点用color: green覆盖了它。类似地,在 React 中,覆盖来自上层的某些上下文的唯一方法是将子组件包装到一个具有不同值的上下文提供者中。

在 CSS 中,像 colorbackground-color这样的不同属性不会相互覆盖。你可以将所有<div>color设置为红色,而不会影响background-color。类似地,不同的 React 上下文不会相互覆盖。你使用 createContext()创建的每个上下文都与其他上下文完全分离,并将使用和提供该特定上下文的组件绑定在一起。一个组件可以毫无问题地使用或提供许多不同的上下文。

在使用上下文之前

上下文非常诱人!然而,这也意味着它很容易被过度使用。仅仅因为你需要将某些 props 向下传递多层,并不意味着你应该将该信息放入上下文中。

在使用上下文之前,你应该考虑以下几种替代方案:

  1. 首先尝试传递 props。如果你的组件不是简单的,那么通过十几个组件向下传递十几个 props 并不罕见。这可能感觉像是一项苦差事,但它能非常清晰地表明哪些组件使用了哪些数据!维护你代码的人会很高兴你通过 props 明确了数据流。
  2. 提取组件并将 JSX 作为 children 传递给它们。如果你通过许多不使用该数据(只是将其进一步向下传递)的中间组件层传递某些数据,这通常意味着你在过程中忘记提取一些组件。例如,你可能将像 posts这样的数据 props 传递给不直接使用它们的视觉组件,例如<Layout posts={posts} />。相反,让Layout接受children作为 prop,并渲染<Layout><Posts posts={posts} /></Layout>。这减少了指定数据的组件与需要数据的组件之间的层数。

如果这些方法都不适合你,再考虑使用上下文。

上下文的用例

  • 主题化:如果你的应用允许用户更改其外观(例如深色模式),你可以在应用的顶部放置一个上下文提供者,并在需要调整视觉外观的组件中使用该上下文。
  • 当前账户:许多组件可能需要知道当前登录的用户。将其放入上下文中可以方便地在树中的任何位置读取它。有些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。在这些情况下,将 UI 的一部分包装到具有不同当前账户值的嵌套提供者中会很方便。
  • 路由:大多数路由解决方案在内部使用上下文来保存当前路由。这就是每个链接如何“知道”自己是否处于活动状态。如果你构建自己的路由器,可能也想这样做。
  • 状态管理:随着应用的增长,你可能最终会在应用顶部附近拥有大量状态。下面许多遥远的组件可能想要更改它。通常将 reducer 与上下文结合使用来管理复杂状态并将其传递给遥远的组件,而不会带来太多麻烦。

上下文不限于静态值。如果你在下一次渲染时传递不同的值,React 将更新下面所有读取它的组件!这就是为什么上下文经常与状态结合使用。

一般来说,如果树中不同部分的遥远组件需要某些信息,这很好地表明上下文将对你有所帮助。

回顾

  • 上下文允许组件向其下方的整个树提供一些信息。
  • 传递上下文:
    1. 使用 export const MyContext = createContext(defaultValue)创建并导出它。
    2. 将其传递给useContext(MyContext)Hook,以便在任何子组件中读取它,无论层级多深。
    3. 将子组件包装到<MyContext value={...}>中,以便从父组件提供它。
  • 上下文会穿过中间的任何组件。
  • 上下文允许你编写“适应其周围环境”的组件。
  • 在使用上下文之前,尝试传递 props 或将 JSX 作为children传递。

尝试一些挑战

Challenge 1 of 1:使用上下文替换属性透传 #

在这个例子中,切换复选框会改变传递给每个 imageSize<PlaceImage> 属性。复选框状态保存在顶层的 App 组件中,但每个 <PlaceImage> 都需要知道它。

目前,AppimageSize 传递给 ListList 将其传递给每个 PlacePlace 再将其传递给 PlaceImage。请移除 imageSize 属性,改为从 App 组件直接将其传递给 PlaceImage

你可以在 Context.js 中声明上下文。