使用 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.

Prop drilling

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 中的所有标题具有相同的大小

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

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

仅靠 props 是做不到的。这就是 context 的用武之地。你将分三步完成

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

Context 允许父组件(即使是远处的父组件!)向其内部的整个树提供一些数据。

在近距离子组件中使用 context

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.

在远距离子组件中使用 context

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:创建 context

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

import { createContext } from 'react';

export const LevelContext = createContext(1);

createContext

第 2 步:使用上下文

从 React 和你的上下文中导入 useContext 钩子

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 是一个钩子。就像 useStateuseReducer 一样,你只能在 React 组件内部立即调用一个钩子(不能在循环或条件语句内部)。useContext 告诉 React,Heading 组件想要读取 LevelContext

现在 Heading 组件没有 level prop,你不需要像这样在 JSX 中传递 level prop 给 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:
使用上下文替换 prop drilling

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

目前,AppimageSize 传递给 ListList 又将其传递给每个 PlacePlace 再将其传递给 PlaceImage。删除 imageSize 属性,而是将其从 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}
    />
  );
}