使用上下文(context)深度传递数据
通常,您会通过 props 将信息从父组件传递到子组件。 但是如果你必须在中间通过许多组件传递它们,或者如果你的应用程序中的许多组件需要相同的信息,那么传递 props 会变得冗长和不方便。 Context 允许父组件向其下方树中的任何组件提供一些信息——无论多深——而无需通过 props 显式传递。
你将学习
- 什么是“ prop钻孔”
- 如何用上下文替换重复的道具传递
- 上下文的常见用例
- 上下文的常见替代方法
传递props的问题
传递 props 是通过 UI 树将数据显式传输到使用它的组件的好方法。
但是当你需要通过树深入传递一些 prop 时,或者如果许多组件需要相同的 prop 时,传递 props 会变得冗长和不方便。 最近的共同祖先可能远离需要数据的组件,并且将状态提升到那么高可能导致有时称为“prop钻孔”的情况。

如果有一种方法可以在不传递 props 的情况下将数据“传送”到树中需要它的组件,那不是很好吗? 有了 React 的上下文功能,就有了!
上下文:传递道具的替代方法
上下文让父组件向其下方的整个树提供数据。 上下文有很多用途。 这是一个例子。 考虑这个接受其大小级别的 Heading 组件:
Heading.js
export default function Heading({ level, children }) {
switch (level) {
case 1:
return <h1>{children}</h1>;
case 2:
return <h2>{children}</h2>;
case 3:
return <h3>{children}</h3>;
case 4:
return <h4>{children}</h4>;
case 5:
return <h5>{children}</h5>;
case 6:
return <h6>{children}</h6>;
default:
throw Error('Unknown level: ' + level);
}
}
Section.js
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
App.js
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>
);
}
假设您希望同一部分中的多个标题始终具有相同的大小:
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> :
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
如果您可以将 level 属性传递给 <Section> 组件并将其从 <Heading> 中删除,那就太好了。 这样您就可以强制同一部分中的所有标题具有相同的大小:
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
但是 <Heading> 组件怎么知道它最近的 <Section> 的级别呢? 这将需要某种方式让孩子从树上方的某个地方“询问”数据。
单靠props是做不到的。 这就是上下文发挥作用的地方。 您将分三步完成:
- 创建上下文。 (您可以将其称为 LevelContext,因为它用于标题级别。)
- 使用来自需要数据的组件的上下文。 (标题将使用 LevelContext。)
- 从指定数据的组件中提供该上下文。 (部分将提供 LevelContext。)
上下文让父级——即使是远方的父级!——向其中的整棵树提供一些数据。

第 1 步:创建上下文
LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1);
Heading.js
export default function Heading({ level, children }) {
switch (level) {
case 1:
return <h1>{children}</h1>;
case 2:
return <h2>{children}</h2>;
case 3:
return <h3>{children}</h3>;
case 4:
return <h4>{children}</h4>;
case 5:
return <h5>{children}</h5>;
case 6:
return <h6>{children}</h6>;
default:
throw Error('Unknown level: ' + level);
}
}
Section.js
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
App.js
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>
);
}
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 属性并从您刚刚导入的上下文 LevelContext 中读取值:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext 是一个钩子。 就像 useState 和 useReducer 一样,你只能在 React 组件的顶层调用 Hook。 useContext 告诉 React Heading 组件想要读取 LevelContext。
现在 Heading 组件没有 level 属性,你不需要再像这样在你的 JSX 中将 level 属性传递给 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>
提醒一下,这是您试图开始工作的标记。
请注意,此示例还不能正常工作! 所有标题都具有相同的大小,因为即使您正在使用上下文,您还没有提供它。 React 不知道从哪里得到它!
如果您不提供上下文,React 将使用您在上一步中指定的默认值。 在此示例中,您将 1 指定为 createContext 的参数,因此 useContext(LevelContext) 返回 1,将所有这些标题设置为 <h1>。 让我们通过让每个部分提供自己的上下文来解决这个问题。
第 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,请给他们这个级别。” 该组件将使用其上方的 UI 树中最近的 <LevelContext.Provider> 的值。
LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1);
Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Heading({ children }) {
const level = useContext(LevelContext);
switch (level) {
case 1:
return <h1>{children}</h1>;
case 2:
return <h2>{children}</h2>;
case 3:
return <h3>{children}</h3>;
case 4:
return <h4>{children}</h4>;
case 5:
return <h5>{children}</h5>;
case 6:
return <h6>{children}</h6>;
default:
throw Error('Unknown level: ' + level);
}
}
Section.js
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
App.js
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 组件! 相反,它通过询问上面最近的部分来“计算出”它的标题级别:
- 您将level属性传递给 <Section>。
- 部分将其子项包装到 <LevelContext.Provider value={level}> 中。
- Heading 使用 useContext(LevelContext) 询问与上面 LevelContext 最接近的值。
使用和提供来自同一组件的上下文
目前,您仍然需要手动指定每个部分的级别:
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
由于上下文允许您从上面的组件读取信息,因此每个部分都可以从上面的部分读取级别,并自动向下传递级别 + 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 属性传递给 <Section> 或 <Heading>:
LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(0);
Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Heading({ children }) {
const level = useContext(LevelContext);
switch (level) {
case 0:
throw Error('Heading must be inside a Section!');
case 1:
return <h1>{children}</h1>;
case 2:
return <h2>{children}</h2>;
case 3:
return <h3>{children}</h3>;
case 4:
return <h4>{children}</h4>;
case 5:
return <h5>{children}</h5>;
case 6:
return <h6>{children}</h6>;
default:
throw Error('Unknown level: ' + level);
}
}
Section.js
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>
);
}
App.js
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>
);
}
现在 Heading 和 Section 都读取 LevelContext 来弄清楚它们有多“深”。 Section 将其子级包装到 LevelContext 中,以指定其中的任何内容都处于“更深”级别。
此示例使用标题级别,因为它们直观地显示了嵌套组件如何覆盖上下文。 但是上下文对于许多其他用例也很有用。 您可以使用它来传递整个子树所需的任何信息:当前颜色主题、当前登录的用户等等。
上下文通过中间组件
您可以在提供上下文的组件和使用它的组件之间插入任意数量的组件。 这包括像 <div> 这样的内置组件和您可能自己构建的组件。
在此示例中,相同的 Post 组件(带有虚线边框)在两个不同的嵌套级别呈现。 请注意,其中的 <Heading> 自动从最近的 <Section> 获取其级别:
Section.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children, isFancy }) {
const level = useContext(LevelContext);
return (
<section className={
'section ' +
(isFancy ? 'fancy' : '')
}>
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
App.js
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 中,颜色和背景颜色等不同的属性不会相互覆盖。 您可以将所有 <div> 的颜色设置为红色而不影响背景颜色。 同样,不同的 React 上下文不会相互覆盖。 您使用 createContext() 创建的每个上下文都完全独立于其他上下文,并将使用和提供该特定上下文的组件联系在一起。 一个组件可以毫无问题地使用或提供许多不同的上下文。
使用上下文之前
上下文非常诱人使用! 然而,这也意味着它很容易被过度使用。仅仅因为您需要将一些 props 传递到多个层次,并不意味着您应该将这些信息放入上下文中。
在使用上下文之前,您应该考虑以下几个备选方案:
- 从传递属性开始。 如果您的组件不是微不足道的,那么通过十几个组件向下传递一打props并不罕见。 它可能感觉像一个 slog,但它非常清楚哪些组件使用哪些数据! 维护你的代码的人会很高兴你已经使用 props 明确了数据流。
- 提取组件并将 JSX 作为子组件传递给它们。 如果您将某些数据传递给许多不使用该数据的中间组件层(并且仅将其进一步向下传递),这通常意味着您忘记沿途提取某些组件。 例如,也许您将像帖子这样的数据道具传递给不直接使用它们的可视化组件,例如 <Layout posts={posts} />。 相反,让 Layout 将 children 作为 prop,并渲染 <Layout><Posts posts={posts} /></Layout>。 这减少了指定数据的组件和需要数据的组件之间的层数。
如果这些方法都不适合您,请考虑上下文。
上下文用例
- 主题:如果您的应用允许用户更改其外观(例如暗模式),您可以将上下文提供程序放在应用的顶部,并在需要调整其视觉外观的组件中使用该上下文。
- 当前帐户:许多组件可能需要知道当前登录的用户。 将它放在上下文中可以方便地在树中的任何位置阅读它。 某些应用程序还允许您同时操作多个帐户(例如,以不同用户的身份发表评论)。 在这些情况下,将 UI 的一部分包装到具有不同当前帐户值的嵌套提供程序中会很方便。
- 路由:大多数路由解决方案在内部使用上下文来保存当前路由。 这就是每个链接“知道”它是否处于活动状态的方式。 如果您构建自己的路由器,您可能也想这样做。
- 管理状态:随着您的应用程序的增长,您最终可能会在靠近应用程序顶部的地方看到很多状态。 下面的许多远程组件可能想要更改它。 通常将 reducer 与上下文一起使用来管理复杂状态并将其传递给远程组件而不会太麻烦。
上下文不限于静态值。 如果你在下一次渲染时传递一个不同的值,React 将更新下面读取它的所有组件! 这就是上下文经常与状态结合使用的原因。
通常,如果树的不同部分中的远距离组件需要某些信息,则表明上下文可以帮助您。
回顾
- 上下文让组件向其下方的整个树提供一些信息。
- 传递上下文:
- 使用 export const MyContext = createContext(defaultValue) 创建并导出它。
- 将它传递给 useContext(MyContext) Hook 以在任何子组件中读取它,无论多深。
- 将子项包装到 <MyContext.Provider value={...}> 中以从父项提供它。
- 上下文通过中间的任何组件。
- 上下文使您可以编写“适应周围环境”的组件。
- 在使用上下文之前,尝试传递 props 或将 JSX 作为子对象传递。
网友评论