跳到主要内容

使用 Context 深层传递参数

  • 通常,通过 props 从父组件向子组件传递参数。
  • 但是,如果你必须通过许多中间组件向下传递 props ,或是在你应用中的许多组件需要相同的信息,传递 props 会变的十分冗长和不便。
  • Context 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递。

Context:传递 props 的另一种方法

  • Context 让父组件可以为它下面的整个组件树提供数据。
  • Context 有很多种用途。
  • 这里就有一个示例。
  • 思考一下这个 Heading 组件接收一个 level 参数来决定它标题尺寸的场景:
App.jsx
import Heading from './Heading.js'
import Section from './Section.js'

export default function Page() {
return (
<Section>
<Heading level={1}>主标题</Heading>
<Section>
<Heading level={2}>副标题</Heading>
<Heading level={2}>副标题</Heading>
<Heading level={2}>副标题</Heading>
<Section>
<Heading level={3}>子标题</Heading>
<Heading level={3}>子标题</Heading>
<Heading level={3}>子标题</Heading>
<Section>
<Heading level={4}>子子标题</Heading>
<Heading level={4}>子子标题</Heading>
<Heading level={4}>子子标题</Heading>
</Section>
</Section>
</Section>
</Section>
)
}
Section.jsx
export default function Section({ children }) {
return <section className="section">{children}</section>
}
Heading.jsx
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('未知的 level:' + level)
}
}
  • 不通过 props 来实现。通过 context 实现方法。可以通过以下三个步骤来实现:
    • 1. 创建 一个 context。(你可以将其命名为 LevelContext, 因为它表示的是标题级别。
    • 在需要数据的组件内 2. 使用 刚刚创建的 context。(Heading 将会使用 LevelContext。)
    • 在指定数据的组件中 3. 提供 这个 context。 (Section 将会提供 LevelContext。)
  • Context 可以让父节点,甚至是很远的父节点都可以为其内部的整个组件树提供数据。

Step 1:创建 Context,createContext

  • 首先,你需要创建这个 context,并 将其从一个文件中导出,这样你的组件才可以使用它:
  • createContext 只需默认值这么一个参数。在这里, 1 表示最大的标题级别,但是你可以传递任何类型的值(甚至可以传入一个对象)。
  • 你将在下一个步骤中见识到默认值的意义。
独立文件 LevelContext.js,创建context 上下文
import { createContext } from 'react'
// 1 为默认值
export const LevelContext = createContext(1)

Step 2:使用 Context,useContext

从 React 中引入 useContext Hook 以及你刚刚创建的 context:

import { useContext } from 'react'
import { LevelContext } from './LevelContext.js'
在组件中使用 context 上下文
export default function Heading({ children }) {
const level = useContext(LevelContext)
// ...
}
  • useContext 是一个 Hook。
  • 和 useState 以及 useReducer 一样,你只能在 React 组件中(不是循环或者条件里)立即调用 Hook。
  • useContext 告诉 React Heading 组件想要读取 LevelContext。
<Section level={4}>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
</Section>

Step 3:提供 Context,Provider

Provider 提供者

使用 YourContext.Provider 提供 context 上下文
import { LevelContext } from './LevelContext.js'

export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>{children}</LevelContext.Provider>
</section>
)
}

直接不用写参数,做法

提供值改成 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>
)
}
这样Section也不用传递参数了。
import Heading from './Heading.js'
import Section from './Section.js'

export default function Page() {
return (
<Section>
<Heading>主标题</Heading>
<Section>
<Heading>副标题</Heading>
<Heading>副标题</Heading>
<Heading>副标题</Heading>
<Section>
<Heading>子标题</Heading>
<Heading>子标题</Heading>
<Heading>子标题</Heading>
<Section>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
</Section>
</Section>
</Section>
</Section>
)
}
Heading.jsx
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 必须在 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('未知的 level:' + level)
}
}

Context 会穿过中间层级的组件

  • 你可以在提供 context 的组件和使用它的组件之间的层级插入任意数量的组件。
  • 这包括像 <div> 这样的内置组件和你自己创建的组件。
App.jsx
import Heading from './Heading.js'
import Section from './Section.js'

export default function ProfilePage() {
return (
<Section>
<Heading>My Profile</Heading>
<Post title="旅行者,你好!" body="来看看我的冒险。" />
<AllPosts />
</Section>
)
}

function AllPosts() {
return (
<Section>
<Heading>帖子</Heading>
<RecentPosts />
</Section>
)
}

function RecentPosts() {
return (
<Section>
<Heading>最近的帖子</Heading>
<Post title="里斯本的味道" body="...那些蛋挞!" />
<Post title="探戈节奏中的布宜诺斯艾利斯" body="我爱它!" />
</Section>
)
}

function Post({ title, body }) {
return (
<Section isFancy={true}>
<Heading>{title}</Heading>
<p>
<i>{body}</i>
</p>
</Section>
)
}
Section.jsx
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>
)
}
  • 你不需要做任何特殊的操作。 Section 为它内部的树指定一个 context ,所以你可以在任何地方插入一个 <Heading>,而且它会有正确的尺寸。

  • Context 让你可以编写“适应周围环境”的组件,并且根据 在哪 (或者说 在哪个 context 中)来渲染它们不同的样子。

  • Context 的工作方式可能会让你想起 CSS 属性继承。在 CSS 中,你可以为一个 <div> 手动指定 color: blue,并且其中的任何 DOM 节点,无论多深,都会继承那个颜色,除非中间的其他 DOM 节点用 color: green 来覆盖它。类似地,在 React 中,覆盖来自上层的某些 context 的唯一方法是将子组件包裹到一个提供不同值的 context provider 中。

  • CSS 中,诸如 colorbackground-color 之类的不同属性不会覆盖彼此。你可以设置所有 <div>color 为红色,而不会影响 background-color。类似地,不同的 React context 不会覆盖彼此。你通过 createContext() 创建的每个 context 都和其他 context 完全分离,只有使用和提供 那个特定的 context 的组件才会联系在一起。一个组件可以轻松地使用或者提供许多不同的 context。

写在你使用 context 之前

  • 使用 Context 看起来非常诱人!然而,这也意味着它也太容易被过度使用了。如果你只想把一些 props 传递到多个层级中,这并不意味着你需要把这些信息放到 context 里。

在使用 context 之前,你可以考虑以下几种替代方案:

    1. 从 传递 props 开始。
  • 如果你的组件看起来不起眼,那么通过十几个组件向下传递一堆 props 并不罕见。

  • 这有点像是在埋头苦干,但是这样做可以让哪些组件用了哪些数据变得十分清晰!维护你代码的人会很高兴你用 props 让数据流变得更加清晰。

    1. 抽象组件 并将 JSX 作为 children 传递给它们。
  • 如果你通过很多层不使用该数据的中间组件(并且只会向下传递)来传递数据,这通常意味着你在此过程中忘记了抽象组件。

  • 举个例子,你可能想传递一些像 posts 的数据 props 到不会直接使用这个参数的组件,类似 <Layout posts={posts} />

  • 取而代之的是,让 Layoutchildren 当做一个参数,然后渲染 <Layout><Posts posts={posts} /></Layout>

  • 这样就减少了定义数据的组件和使用数据的组件之间的层级。

  • 如果这两种方法都不适合你,再考虑使用 context。

Context 的使用场景

你可以用它来传递整个子树需要的任何信息:当前的 颜色主题当前登录的用户

  1. 主题: 如果你的应用允许用户更改其外观(例如暗夜模式),你可以在应用顶层 放一个 context provider,并在需要调整其外观的组件中使用该 context。
  2. 当前账户
    • 许多组件可能需要知道当前登录的用户信息。
    • 将它放到 context 中可以方便地在树中的任何位置读取它。
    • 某些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。
    • 在这些情况下,将 UI 的一部分包裹到具有不同账户数据的 provider 中会很方便。
  3. 路由
    • 大多数路由解决方案在其内部使用 context 来保存当前路由。
    • 这就是每个链接“知道”它是否处于活动状态的方式。
    • 如果你创建自己的路由库,你可能也会这么做。
  4. 状态管理
    • 随着你的应用的增长,最终在靠近应用顶部的位置可能会有很多 state
    • 许多遥远的下层组件可能想要修改它们。
    • 通常 将 reducercontext 搭配使用来管理复杂的状态并将其传递给深层的组件来避免过多的麻烦。

Context 不局限于静态值。如果你在下一次渲染时传递不同的值,React 将会更新读取它的所有下层组件!这就是 context 经常和 state 结合使用的原因。

一般而言,如果树中不同部分的远距离组件需要某些信息,context 将会对你大有帮助。

摘要

Context 使组件向其下方的整个树提供信息。

传递 Context 的方法:

  1. 创建 context,通过 export const MyContext = createContext(defaultValue) 创建并导出 context
  2. 使用 context,在无论层级多深的任何子组件中,把 context 传递给 useContext(MyContext) Hook 来读取它。
  3. 提供 context,在父组件中把 children 包在 <MyContext.Provider value={...}> 中来提供 context
  • Context 会穿过中间的任何组件。
  • Context 可以让你写出 “较为通用” 的组件。
  • 在使用 context 之前,先试试传递 props 或者将 JSX 作为 children 传递。