通常,你会通过 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
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 的用武之地。你将分三步完成
- 创建 一个 context。(你可以将其称为
LevelContext
,因为它用于标题级别。) - 使用 需要数据的组件中的 context。(
Heading
将使用LevelContext
。) - 提供 指定数据的组件中的 context。(
Section
将提供LevelContext
。)
Context 允许父组件(即使是远处的父组件!)向其内部的整个树提供一些数据。
步骤 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
是一个钩子。就像 useState
和 useReducer
一样,你只能在 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
来“找出”它的标题级别
- 你将
level
prop 传递给<Section>
。 Section
将其子组件包装到<LevelContext.Provider value={level}>
中。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> ); }
现在,Heading
和 Section
都读取 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 中,像 color
和 background-color
这样的不同属性不会相互覆盖。您可以将所有 <div>
的 color
设置为红色,而不会影响 background-color
。类似地,不同的 React 上下文不会相互覆盖。 您使用 createContext()
创建的每个上下文都与其他上下文完全分开,并且将使用和提供_该特定_上下文的组件联系在一起。一个组件可以毫无问题地使用或提供许多不同的上下文。
在您使用上下文之前
上下文非常容易被滥用!然而,这也意味着它很容易被过度使用。 仅仅因为您需要将一些 props 传递到多层嵌套的组件中,并不意味着您应该将这些信息放入上下文中。
以下是在使用上下文之前应该考虑的一些替代方案
- 首先尝试 传递 props。 如果您的组件并不简单,那么将十几个 props 向下传递给十几个组件并不罕见。这可能感觉很乏味,但它可以让您非常清楚地了解哪些组件使用了哪些数据!维护您代码的人会很高兴您使用 props 明确了数据流。
- 提取组件并将 JSX 作为
children
传递给它们。 如果您将一些数据传递给多层不使用该数据的中间组件(并且只将其进一步向下传递),这通常意味着您忘记了沿途提取一些组件。例如,您可能会将posts
之类的 props 传递给不直接使用它们的视觉组件,例如<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 作为
children
传递。
挑战 1之 1: 使用上下文替换 prop drilling
在此示例中,切换复选框会更改传递给每个 <PlaceImage>
的 imageSize
属性。复选框状态保存在顶级 App
组件中,但每个 <PlaceImage>
都需要知道它。
目前,App
将 imageSize
传递给 List
,List
又将其传递给每个 Place
,Place
再将其传递给 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} /> ); }