使用 Context 深度传递数据

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

你将学习

  • 什么是“prop drilling”(属性穿透)
  • 如何使用 context 替换重复的 prop 传递
  • context 的常见用例
  • context 的常见替代方案

传递 props 的问题

传递 props 是一种通过 UI 树显式地将数据传递到使用它的组件的好方法。

但是,当需要将某个 prop 深度传递到树中,或者许多组件需要相同的 prop 时,传递 props 可能会变得冗长且不方便。最近的公共祖先可能距离需要数据的组件很远,而将状态提升到那么高的位置可能会导致所谓的“prop drilling”(属性穿透)。

提升状态

Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in purple. The value flows down to each of the two children, both highlighted in purple.
Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in purple. The value flows down to each of the two children, both highlighted in purple.

属性穿透

Diagram with a tree of ten nodes, each node with two children or less. The root node contains a bubble representing a value highlighted in purple. The value flows down through the two children, each of which pass the value but do not contain it. The left child passes the value down to two children which are both highlighted purple. The right child of the root passes the value through to one of its two children - the right one, which is highlighted purple. That child passed the value through its single child, which passes it down to both of its two children, which are highlighted purple.
Diagram with a tree of ten nodes, each node with two children or less. The root node contains a bubble representing a value highlighted in purple. The value flows down through the two children, each of which pass the value but do not contain it. The left child passes the value down to two children which are both highlighted purple. The right child of the root passes the value through to one of its two children - the right one, which is highlighted purple. That child passed the value through its single child, which passes it down to both of its two children, which are highlighted purple.

如果没有一种方法可以将数据“传送”到树中需要它的组件而无需传递 props,那岂不是很好?使用 React 的 context 功能,就可以做到!

Context:传递 props 的替代方案

Context 允许父组件向其下方整个树提供数据。Context 有许多用途。以下是一个示例。考虑一下这个 Heading 组件,它接受一个 level 来确定其大小

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>Title</Heading>
      <Heading level={2}>Heading</Heading>
      <Heading level={3}>Sub-heading</Heading>
      <Heading level={4}>Sub-sub-heading</Heading>
      <Heading level={5}>Sub-sub-sub-heading</Heading>
      <Heading level={6}>Sub-sub-sub-sub-heading</Heading>
    </Section>
  );
}

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

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>Title</Heading>
      <Section>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Section>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Section>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

目前,你分别将 level prop 传递给每个 <Heading>

<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>

最好是将 level prop 传递给 <Section> 组件,并将其从 <Heading> 中移除。这样,你可以强制同一部分中的所有标题都具有相同的大小。

<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>

但是 <Heading> 组件如何知道其最近的 <Section> 的级别?这需要某种方式让子组件“请求”来自树上方的某个地方的数据。

你不能只用 props 来做到这一点。这就是 context 发挥作用的地方。你将分三个步骤完成:

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

上下文允许父组件(即使是较远的父组件!)向其内部的整棵树提供一些数据。

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

Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in orange which projects down to the two children, each highlighted in orange.
Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in orange which projects down to the two children, each highlighted in orange.

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

Diagram with a tree of ten nodes, each node with two children or less. The root parent node contains a bubble representing a value highlighted in orange. The value projects down directly to four leaves and one intermediate component in the tree, which are all highlighted in orange. None of the other intermediate components are highlighted.
Diagram with a tree of ten nodes, each node with two children or less. The root parent node contains a bubble representing a value highlighted in orange. The value projects down directly to four leaves and one intermediate component in the tree, which are all highlighted in orange. None of the other intermediate components are highlighted.

步骤 1:创建上下文

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

import { createContext } from 'react';

export const LevelContext = createContext(1);

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

步骤 2:使用上下文

从 React 和你的上下文导入 useContext Hook。

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

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

export default function Heading({ level, children }) {
// ...
}

相反,移除 level prop 并从你刚刚导入的上下文 LevelContext 中读取值。

export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}

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

现在 Heading 组件没有 level prop 了,你就不需要像这样再将 level prop 传递到你的 JSX 中的 Heading 了。

<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>

更新 JSX,以便由 Section 来接收它。

<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>

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

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section level={1}>
      <Heading>Title</Heading>
      <Section level={2}>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section level={3}>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section level={4}>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

注意,这个例子还不能完全工作!所有标题的大小都相同,因为即使你正在使用上下文,你还没有提供它。React不知道从哪里获取它!

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

步骤 3:提供上下文

Section 组件当前正在渲染其子组件。

export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}

用上下文提供程序包装它们,以便向它们提供 LevelContext

import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}

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

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section level={1}>
      <Heading>Title</Heading>
      <Section level={2}>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section level={3}>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section level={4}>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

这与原始代码的结果相同,但是你不需要将 level prop 传递给每个 Heading 组件!相反,它通过询问上面最近的 Section 来“确定”其标题级别。

  1. 你将 level prop 传递给 <Section>
  2. Section 将其子组件包装到 <LevelContext.Provider value={level}> 中。
  3. Heading 使用 useContext(LevelContext) 查询上面最近的 LevelContext 值。

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

目前,您仍然需要手动指定每个部分的level

export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...

由于上下文允许您从上层组件读取信息,因此每个Section都可以从上层Section读取level,并自动向下传递level + 1。以下是您可以执行此操作的方法

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}

通过此更改,您无需将level prop 传递给<Section><Heading>

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

现在,HeadingSection都读取LevelContext以确定它们的嵌套深度。而Section将其子元素包装到LevelContext中,以指定其内部的所有内容都位于更深的级别。

注意

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

上下文会传递中间组件

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

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

import Heading from './Heading.js';
import Section from './Section.js';

export default function ProfilePage() {
  return (
    <Section>
      <Heading>My Profile</Heading>
      <Post
        title="Hello traveller!"
        body="Read about my adventures."
      />
      <AllPosts />
    </Section>
  );
}

function AllPosts() {
  return (
    <Section>
      <Heading>Posts</Heading>
      <RecentPosts />
    </Section>
  );
}

function RecentPosts() {
  return (
    <Section>
      <Heading>Recent Posts</Heading>
      <Post
        title="Flavors of Lisbon"
        body="...those pastéis de nata!"
      />
      <Post
        title="Buenos Aires in the rhythm of tango"
        body="I loved it!"
      />
    </Section>
  );
}

function Post({ title, body }) {
  return (
    <Section isFancy={true}>
      <Heading>
        {title}
      </Heading>
      <p><i>{body}</i></p>
    </Section>
  );
}

您无需为此做任何特殊操作即可使其工作。Section指定其内部树的上下文,因此您可以随时插入<Heading>,它将具有正确的尺寸。在上面的沙箱中试一试!

上下文允许您编写能够“适应其周围环境”的组件,并根据其位置(或者换句话说,在哪个上下文中)进行不同的显示。

上下文的工作方式可能会让您想起CSS 属性继承。在 CSS 中,您可以为<div>指定color: blue,并且其内部的任何 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} />。相反,让 Layoutchildren 作为 prop,并渲染 <Layout><Posts posts={posts} /></Layout>。这减少了指定数据的组件和需要它的组件之间的层数。

如果这两种方法都不适合你,请考虑使用上下文。

上下文的使用案例

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

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

一般来说,如果树的不同部分中的较远组件需要某些信息,则表明上下文将帮助你。

总结

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

挑战 1 1:
使用上下文替换 props 传递

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

目前,AppimageSize 传递给 List,后者将其传递给每个 Place,后者将其传递给 PlaceImage。移除 imageSize prop,而是将其从 App 组件直接传递到 PlaceImage

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

import { useState } from 'react';
import { places } from './data.js';
import { getImageUrl } from './utils.js';

export default function App() {
  const [isLarge, setIsLarge] = useState(false);
  const imageSize = isLarge ? 150 : 100;
  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={isLarge}
          onChange={e => {
            setIsLarge(e.target.checked);
          }}
        />
        Use large images
      </label>
      <hr />
      <List imageSize={imageSize} />
    </>
  )
}

function List({ imageSize }) {
  const listItems = places.map(place =>
    <li key={place.id}>
      <Place
        place={place}
        imageSize={imageSize}
      />
    </li>
  );
  return <ul>{listItems}</ul>;
}

function Place({ place, imageSize }) {
  return (
    <>
      <PlaceImage
        place={place}
        imageSize={imageSize}
      />
      <p>
        <b>{place.name}</b>
        {': ' + place.description}
      </p>
    </>
  );
}

function PlaceImage({ place, imageSize }) {
  return (
    <img
      src={getImageUrl(place)}
      alt={place.name}
      width={imageSize}
      height={imageSize}
    />
  );
}