使用上下文深入传递数据

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

🌐 Usually, you will pass information from a parent component to a child component via props. But passing props can become verbose and inconvenient if you have to pass them through many components in the middle, or if many components in your app need the same information. Context lets the parent component make some information available to any component in the tree below it—no matter how deep—without passing it explicitly through props.

你将学习到

  • 什么是“prop drilling”
  • 如何用上下文替换重复的属性传递
  • 上下文的常见用例
  • 上下文的常见替代方法

传递属性的问题

🌐 The problem with passing props

传递 props 是一种非常好的方法,可以将数据明确地通过你的 UI 树传递给使用它的组件。

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

🌐 But passing props can become verbose and inconvenient when you need to pass some prop deeply through the tree, or if many components need the same prop. The nearest common ancestor could be far removed from the components that need data, and lifting state up that high can lead to a situation called “prop drilling”.

提升状态

🌐 Lifting state up

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 的上下文功能,就可以做到!

🌐 Wouldn’t it be great if there were a way to “teleport” data to the components in the tree that need it without passing props? With React’s context feature, there is!

上下文:传递 props 的一种替代方法

🌐 Context: an alternative to passing props

上下文允许父组件向其下方的整个组件树提供数据。上下文有很多用途。这里有一个例子。考虑这个 Heading 组件,它接受一个 level 作为其大小:

🌐 Context lets a parent component provide data to the entire tree below it. There are many uses for context. Here is one example. Consider this Heading component that accepts a level for its size:

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 中的多个标题始终具有相同的大小:

🌐 Let’s say you want multiple headings within the same Section to always have the same size:

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 属性单独传递给每个 <Heading>

🌐 Currently, you pass the level prop to each <Heading> separately:

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

如果你能将 level 属性传递给 <Section> 组件,而从 <Heading> 中移除它,那会很好。这样你就可以确保同一部分的所有标题具有相同的大小:

🌐 It would be nice if you could pass the level prop to the <Section> component instead and remove it from the <Heading>. This way you could enforce that all headings in the same section have the same size:

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

但是 <Heading> 组件怎么能知道它最近的 <Section> 的层级呢?那就需要某种方式让子组件从树上方的某处“请求”数据。

🌐 But how can the <Heading> component know the level of its closest <Section>? That would require some way for a child to “ask” for data from somewhere above in the tree.

仅靠属性是做不到的。这就是上下文发挥作用的地方。你将分三步来做:

🌐 You can’t do it with props alone. This is where context comes into play. You will do it in three steps:

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

上下文让父级 - 即使是远方的父级! - 向其中的整棵树提供一些数据。

🌐 Context lets a parent—even a distant one!—provide some data to the entire tree inside of it.

在亲密的子级中使用上下文

🌐 Using context in close children

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.

在遥远的子级身上使用情境

🌐 Using context in distant children

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:创建上下文

🌐 Step 1: Create the context

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

🌐 First, you need to create the context. You’ll need to export it from a file so that your components can use it:

import { createContext } from 'react';

export const LevelContext = createContext(1);

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

🌐 The only argument to createContext is the default value. Here, 1 refers to the biggest heading level, but you could pass any kind of value (even an object). You will see the significance of the default value in the next step.

步骤 2:使用上下文

🌐 Step 2: Use the context

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

🌐 Import the useContext Hook from React and your context:

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

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

🌐 Currently, the Heading component reads level from props:

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

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

🌐 Instead, remove the level prop and read the value from the context you just imported, LevelContext:

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

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

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

🌐 Now that the Heading component doesn’t have a level prop, you don’t need to pass the level prop to Heading in your JSX like this anymore:

<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 接收它,而不是其他组件:

🌐 Update the JSX so that it’s the Section that receives it instead:

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

提醒一下,这是你试图开始工作的标记:

🌐 As a reminder, this is the markup that you were trying to get working:

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 不知道从哪里获取它!

🌐 Notice this example doesn’t quite work, yet! All the headings have the same size because even though you’re using the context, you have not provided it yet. React doesn’t know where to get it!

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

🌐 If you don’t provide the context, React will use the default value you’ve specified in the previous step. In this example, you specified 1 as the argument to createContext, so useContext(LevelContext) returns 1, setting all those headings to <h1>. Let’s fix this problem by having each Section provide its own context.

步骤 3:提供上下文

🌐 Step 3: Provide the context

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

🌐 The Section component currently renders its children:

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 value={level}>
{children}
</LevelContext>
</section>
);
}

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

🌐 This tells React: “if any component inside this <Section> asks for LevelContext, give them this level.” The component will use the value of the nearest <LevelContext> in the UI tree above it.

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 属性传递给每个 Heading 组件!相反,它通过询问上方最近的 Section 来“确定”其标题级别:

🌐 It’s the same result as the original code, but you did not need to pass the level prop to each Heading component! Instead, it “figures out” its heading level by asking the closest Section above:

  1. 你将 level 属性传递给 <Section>
  2. Section 将其子元素封装到 <LevelContext value={level}> 中。
  3. Heading 使用 useContext(LevelContext) 请求上方最接近的 LevelContext 值。

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

🌐 Using and providing context from the same component

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

🌐 Currently, you still have to specify each section’s level manually:

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

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

🌐 Since context lets you read information from a component above, each Section could read the level from the Section above, and pass level + 1 down automatically. Here is how you could do it:

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

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

有了这个更改,你不需要将 level 属性传递给 <Section><Heading>:

🌐 With this change, you don’t need to pass the level prop either to the <Section> or to the <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 中,以指定其中的任何内容处于更“深”的层级。

🌐 Now both Heading and Section read the LevelContext to figure out how “deep” they are. And the Section wraps its children into the LevelContext to specify that anything inside of it is at a “deeper” level.

注意

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

🌐 This example uses heading levels because they show visually how nested components can override context. But context is useful for many other use cases too. You can pass down any information needed by the entire subtree: the current color theme, the currently logged in user, and so on.

中间组件传递上下文

🌐 Context passes through intermediate components

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

🌐 You can insert as many components as you like between the component that provides context and the one that uses it. This includes both built-in components like <div> and components you might build yourself.

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

🌐 In this example, the same Post component (with a dashed border) is rendered at two different nesting levels. Notice that the <Heading> inside of it gets its level automatically from the closest <Section>:

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

export default function ProfilePage() {
  return (
    <Section>
      <Heading>My Profile</Heading>
      <Post
        title="你好,旅行者!"
        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="里斯本的风味"
        body="...those pastéis de nata!"
      />
      <Post
        title="在探戈的节奏中的布宜诺斯艾利斯"
        body="I loved it!"
      />
    </Section>
  );
}

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

你不需要做任何特别的事情就能使它工作。Section 指定了其内部树的上下文,因此你可以在任意位置插入 <Heading>,它都会具有正确的大小。试试上面的沙箱吧!

🌐 You didn’t do anything special for this to work. A Section specifies the context for the tree inside it, so you can insert a <Heading> anywhere, and it will have the correct size. Try it in the sandbox above!

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

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

🌐 How context works might remind you of CSS property inheritance. In CSS, you can specify color: blue for a <div>, and any DOM node inside of it, no matter how deep, will inherit that color unless some other DOM node in the middle overrides it with color: green. Similarly, in React, the only way to override some context coming from above is to wrap children into a context provider with a different value.

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

🌐 In CSS, different properties like color and background-color don’t override each other. You can set all <div>’s color to red without impacting background-color. Similarly, different React contexts don’t override each other. Each context that you make with createContext() is completely separate from other ones, and ties together components using and providing that particular context. One component may use or provide many different contexts without a problem.

使用上下文之前

🌐 Before you use context

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

🌐 Context is very tempting to use! However, this also means it’s too easy to overuse it. Just because you need to pass some props several levels deep doesn’t mean you should put that information into context.

在使用上下文之前,你应该考虑以下几个备选方案:

🌐 Here’s a few alternatives you should consider before using context:

  1. 传递 props 开始. 如果你的组件不是简单的,把十几个 props 通过十几个组件传递下去是很常见的。虽然可能感觉有点麻烦,但它能非常清楚地表明哪些组件使用哪些数据!维护你代码的人会很高兴你通过 props 明确了数据流。
  2. 提取组件并将 JSX 作为 children 传递给它们。 如果你通过许多不使用这些数据的中间组件传递一些数据(只是简单地向下传递),这通常意味着你在某些地方忘记提取组件。例如,也许你将像 posts 这样的数据 props 传递给不直接使用它们的可视组件,如 <Layout posts={posts} />。相反,让 Layout 接收 children 作为 prop,并渲染 <Layout><Posts posts={posts} /></Layout>。这会减少指定数据的组件与需要它的组件之间的层数。

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

🌐 If neither of these approaches works well for you, consider context.

上下文用例

🌐 Use cases for context

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

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

🌐 Context is not limited to static values. If you pass a different value on the next render, React will update all the components reading it below! This is why context is often used in combination with state.

一般而言,如果树的不同部分中的远距离组件需要某些信息,则这是一个很好的指示,上下文可以帮助你。

🌐 In general, if some information is needed by distant components in different parts of the tree, it’s a good indication that context will help you.

回顾

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

挑战 1 of 1:
用上下文替换属性钻取

🌐 Replace prop drilling with context

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

🌐 In this example, toggling the checkbox changes the imageSize prop passed to each <PlaceImage>. The checkbox state is held in the top-level App component, but each <PlaceImage> needs to be aware of it.

当前,AppimageSize 传递给 ListList 将其传递给每个 PlacePlace 再传递给 PlaceImage。移除 imageSize 属性,而是直接从 App 组件传递给 PlaceImage

🌐 Currently, App passes imageSize to List, which passes it to each Place, which passes it to the PlaceImage. Remove the imageSize prop, and instead pass it from the App component directly to PlaceImage.

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

🌐 You can declare context in 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}
    />
  );
}