通常,你会通过属性将信息从父组件传递到子组件。但是如果你必须在中间通过许多组件传递它们,或者如果你的应用中的许多组件需要相同的信息,那么传递属性会变得冗长和不方便。Context 允许父组件向其下面的树中的任何组件提供一些信息(无论多深),而无需通过属性显式传递。
¥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.
你将学习到
-
什么是 “属性钻取”
¥What “prop drilling” is
-
如何用上下文替换重复的属性传递
¥How to replace repetitive prop passing with context
-
上下文的常见用例
¥Common use cases for context
-
上下文的常见替代方法
¥Common alternatives to context
传递属性的问题
¥The problem with passing props
传递属性 是通过 UI 树将数据显式传输到使用它的组件的好方法。
¥Passing props is a great way to explicitly pipe data through your UI tree to the components that use it.
但是当你需要通过树深入传递一些属性时,或者如果许多组件需要相同的属性时,传递属性会变得冗长和不方便。最近的共同祖级可能远离需要数据的组件,那么高的 提升状态 可能导致称为 “属性钻取” 的情况。
¥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”.
如果有一种方法可以在不传递属性的情况下 “远距离传送” 数据给树中需要它的组件,那不是很好吗?有了 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!
上下文:传递属性的替代方案
¥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:
-
创建一个上下文。(你可以将其称为
LevelContext
,因为它用于标题级别。)¥Create a context. (You can call it
LevelContext
, since it’s for the heading level.) -
使用需要数据的组件中的上下文。(
Heading
将使用LevelContext
。)¥Use that context from the component that needs the data. (
Heading
will useLevelContext
.) -
从指定数据的组件提供上下文。(
Section
将提供LevelContext
。)¥Provide that context from the component that specifies the data. (
Section
will provideLevelContext
.)
上下文让父级 - 即使是远方的父级! - 向其中的整棵树提供一些数据。
¥Context lets a parent—even a distant one!—provide some data to the entire tree inside of it.
步骤 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
钩子:
¥Import the useContext
Hook from React and your context:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
目前,Heading
组件从属性中读取 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
是一个钩子。就像 useState
和 useReducer
一样,你只能在 React 组件内立即调用钩子(不能在循环或条件内)。useContext
告诉 React Heading
组件想要读取 LevelContext
。
¥useContext
is a Hook. Just like useState
and useReducer
, you can only call a Hook immediately inside a React component (not inside loops or conditions). useContext
tells React that the Heading
component wants to read the LevelContext
.
现在 Heading
组件没有 level
属性,你不需要再像这样在你的 JSX 中将 level 属性传递给 Heading
了:
¥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
:
¥Wrap them with a context provider to provide the LevelContext
to them:
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>
的值。
¥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.Provider>
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:
-
你将
level
属性传递给<Section>
。¥You pass a
level
prop to the<Section>
. -
Section
将其子级封装到<LevelContext.Provider value={level}>
中。¥
Section
wraps its children into<LevelContext.Provider value={level}>
. -
Heading
使用useContext(LevelContext)
询问上面的LevelContext
最接近的值。¥
Heading
asks the closest value ofLevelContext
above withuseContext(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.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</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> ); }
现在 Heading
和 Section
都读取了 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.
中间组件传递上下文
¥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="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>
,并且它将具有正确的大小。在上面的沙盒中试试吧!
¥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 lets you write components that “adapt to their surroundings” and display themselves differently depending on where (or, in other words, in which context) they are being rendered.
上下文的工作方式可能会让你想起 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 中,不同的属性(如 color
和 background-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 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:
-
从 传递属性 开始 如果你的组件并不简单,那么通过十几个组件传递十几个属性并不罕见。它可能感觉像长途跋涉,但它非常清楚哪些组件使用哪些数据!维护你的代码的人会很高兴你已经使用属性明确了数据流。
¥Start by passing props. If your components are not trivial, it’s not unusual to pass a dozen props down through a dozen components. It may feel like a slog, but it makes it very clear which components use which data! The person maintaining your code will be glad you’ve made the data flow explicit with props.
-
提取组件并 将 JSX 作为
children
传递 给它们。如果你通过许多不使用该数据的中间组件层传递某些数据(并且仅将其进一步向下传递),这通常意味着你忘记沿途提取某些组件。例如,也许你将posts
之类的数据属性传递给不直接使用它们的可视化组件,例如<Layout posts={posts} />
。而是,让Layout
将children
作为属性,并渲染<Layout><Posts posts={posts} /></Layout>
。这减少了指定数据的组件和需要数据的组件之间的层数。¥Extract components and pass JSX as
children
to them. If you pass some data through many layers of intermediate components that don’t use that data (and only pass it further down), this often means that you forgot to extract some components along the way. For example, maybe you pass data props likeposts
to visual components that don’t use them directly, like<Layout posts={posts} />
. Instead, makeLayout
takechildren
as a prop, and render<Layout><Posts posts={posts} /></Layout>
. This reduces the number of layers between the component specifying the data and the one that needs it.
如果这些方法都不适合你,请考虑上下文。
¥If neither of these approaches works well for you, consider context.
上下文用例
¥Use cases for context
-
主题:如果你的应用允许用户更改其外观(例如夜间模式),你可以将上下文提供器放在应用的顶部,并在需要调整其视觉外观的组件中使用该上下文。
¥Theming: If your app lets the user change its appearance (e.g. dark mode), you can put a context provider at the top of your app, and use that context in components that need to adjust their visual look.
-
当前账户:许多组件可能需要知道当前登录的用户。将它放在上下文中可以方便地在树中的任何位置读取它。某些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。在这些情况下,将 UI 的一部分封装到具有不同当前账户值的嵌套提供器中会很方便。
¥Current account: Many components might need to know the currently logged in user. Putting it in context makes it convenient to read it anywhere in the tree. Some apps also let you operate multiple accounts at the same time (e.g. to leave a comment as a different user). In those cases, it can be convenient to wrap a part of the UI into a nested provider with a different current account value.
-
路由大多数路由解决方案在内部使用上下文来保存当前路由。这就是每个链接 “知道” 是否处于活动状态的方式。如果你构建自己的路由,你可能也想这样做。
¥Routing: Most routing solutions use context internally to hold the current route. This is how every link “knows” whether it’s active or not. If you build your own router, you might want to do it too.
-
管理状态:随着你的应用的增长,你最终可能会在靠近应用顶部的地方看到很多状态。下面的许多远程组件可能想要更改它。将 reducer 与上下文一起使用 经常管理复杂的状态并将其传递给远程组件而没有太多麻烦。
¥Managing state: As your app grows, you might end up with a lot of state closer to the top of your app. Many distant components below may want to change it. It is common to use a reducer together with context to manage complex state and pass it down to distant components without too much hassle.
上下文不限于静态值。如果你在下一次渲染时传递一个不同的值,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.
回顾
-
上下文让组件向其下方的整个树提供一些信息。
¥Context lets a component provide some information to the entire tree below it.
-
传递上下文:
¥To pass context:
-
使用
export const MyContext = createContext(defaultValue)
创建并导出它。¥Create and export it with
export const MyContext = createContext(defaultValue)
. -
将它传递给
useContext(MyContext)
钩子以在任何子组件中读取它,无论多深。¥Pass it to the
useContext(MyContext)
Hook to read it in any child component, no matter how deep. -
将子级封装到
<MyContext.Provider value={...}>
中以从父级那里提供。¥Wrap children into
<MyContext.Provider value={...}>
to provide it from a parent.
-
-
上下文贯穿中间的任何组件。
¥Context passes through any components in the middle.
-
上下文使你可以编写 “适应它们的环境” 的组件。
¥Context lets you write components that “adapt to their surroundings”.
-
在使用上下文之前,尝试传递属性或将 JSX 作为
children
传递。¥Before you use context, try passing props or passing JSX as
children
.
挑战 1 / 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.
目前,App
将 imageSize
传递给 List
,List
将其传递给每个 Place
,再将其传递给 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} /> ); }