useContext

useContext 是一个 React Hook,它让你可以从组件中读取并订阅 context

const value = useContext(SomeContext)

参考

useContext(SomeContext)

在组件的顶层调用 useContext,以读取并订阅 context。

import { useContext } from 'react';

function MyComponent() {
const theme = useContext(ThemeContext);
// ...

查看更多示例。

参数

  • SomeContext:你之前通过 createContext 创建的 context。context 本身并不保存信息,它只表示你可以向组件提供或从组件读取的信息类型。

返回值

useContext 返回调用该 Hook 的组件对应的 context 值。它由调用组件在树中上方最近的 SomeContext 提供者传入的 value 决定。如果没有这样的提供者,那么返回值将是你为该 context 传给 createContextdefaultValue。返回值始终是最新的。如果某个 context 发生变化,React 会自动重新渲染读取该 context 的组件。

注意事项

  • 组件中的 useContext() 调用不会受到同一组件返回的提供者影响。对应的 <Context> 需要位于调用 useContext() 的组件上方
  • React 会从接收不同 value 的提供者开始,自动重新渲染所有使用某个特定 context 的子组件。前后两个值会通过 Object.is 进行比较。使用 memo 跳过重新渲染,并不会阻止子组件接收新的 context 值。
  • 如果你的构建系统在输出中产生了重复模块(使用符号链接时可能发生),这会破坏 context。通过 context 传值只有在你用于提供 context 的 SomeContext 和用于读取它的 SomeContext 确实是同一个对象时才有效,这一点由 === 比较决定。

用法

将数据深入传递到树中

在组件的顶层调用 useContext,以读取并订阅 context。

import { useContext } from 'react';

function Button() {
const theme = useContext(ThemeContext);
// ...

useContext 会返回你传入的 context 值,对应于 context。为了确定 context 值,React 会搜索组件树,并找到该特定 context 上方最近的 context 提供者

要向 Button 传递 context,请将它或其某个父组件包裹在对应的 context 提供者中:

function MyPage() {
return (
<ThemeContext value="dark">
<Form />
</ThemeContext>
);
}

function Form() {
// ... 在内部渲染按钮 ...
}

提供者和 Button 之间隔着多少层组件都没关系。当 Form 内部任何地方Button 调用 useContext(ThemeContext) 时,它都会接收到 "dark" 作为值。

Pitfall

useContext() 总是查找调用它的组件上方最近的提供者。它会向上搜索,不会考虑你调用 useContext() 的那个组件内部的提供者。

import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  return (
    <ThemeContext value="dark">
      <Form />
    </ThemeContext>
  )
}

function Form() {
  return (
    <Panel title="欢迎">
      <Button>注册</Button>
      <Button>登录</Button>
    </Panel>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}


更新通过 context 传递的数据

通常,你会希望 context 随时间变化。要更新 context,请将它与 state 结合起来。在父组件中声明一个 state 变量,然后将当前 state 作为 context 值 传给提供者。

function MyPage() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext value={theme}>
<Form />
<Button onClick={() => {
setTheme('light');
}}>
切换到浅色主题
</Button>
</ThemeContext>
);
}

现在,提供者内部的任何 Button 都会接收到当前的 theme 值。如果你调用 setTheme 来更新传给提供者的 theme 值,所有 Button 组件都会随着新的 'light' 值重新渲染。

更新 context 的示例

Example 1 of 5:
通过 context 更新值

在这个示例中,MyApp 组件持有一个 state 变量,然后将其传给 ThemeContext 提供者。勾选“Dark mode”复选框会更新 state。改变所提供的值会使所有使用该 context 的组件重新渲染。

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext value={theme}>
      <Form />
      <label>
        <input
          type="checkbox"
          checked={theme === 'dark'}
          onChange={(e) => {
            setTheme(e.target.checked ? 'dark' : 'light')
          }}
        />
        使用深色模式
      </label>
    </ThemeContext>
  )
}

function Form({ children }) {
  return (
    <Panel title="欢迎">
      <Button>注册</Button>
      <Button>登录</Button>
    </Panel>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}

注意,value="dark" 传递的是 "dark" 字符串,而 value={theme} 通过 JSX 花括号 传递的是 JavaScript 变量 theme 的值。花括号也允许你传递不是字符串的 context 值。


指定回退默认值

如果 React 在父组件树中找不到该特定 context 的任何提供者,那么 useContext() 返回的 context 值将等于你在创建该 context 时指定的 默认值

const ThemeContext = createContext(null);

默认值永远不会改变。如果你想更新 context,请像上面所述那样将它与 state 一起使用。

通常,与其使用 null,不如使用更有意义的默认值,例如:

const ThemeContext = createContext('light');

这样,如果你不小心渲染了某个没有对应提供者的组件,也不会出错。这也有助于你的组件在测试环境中良好工作,而无需在测试里设置大量提供者。

在下面的示例中,“Toggle theme”按钮始终是浅色的,因为它位于任何 theme context 提供者之外,而默认的 context 主题值是 'light'。尝试将默认主题改为 'dark'

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext('light');

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <>
      <ThemeContext value={theme}>
        <Form />
      </ThemeContext>
      <Button onClick={() => {
        setTheme(theme === 'dark' ? 'light' : 'dark');
      }}>
        切换主题
      </Button>
    </>
  )
}

function Form({ children }) {
  return (
    <Panel title="欢迎">
      <Button>注册</Button>
      <Button>登录</Button>
    </Panel>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children, onClick }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className} onClick={onClick}>
      {children}
    </button>
  );
}


覆盖树中某一部分的 context

你可以通过把树的某一部分包裹在具有不同值的提供者中,来覆盖该部分的 context。

<ThemeContext value="dark">
...
<ThemeContext value="light">
<Footer />
</ThemeContext>
...
</ThemeContext>

你可以根据需要多次嵌套并覆盖提供者。

覆盖 context 的示例

Example 1 of 2:
覆盖主题

这里,Footer 内部的按钮接收到的 context 值("light")与外部按钮("dark")不同。

import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  return (
    <ThemeContext value="dark">
      <Form />
    </ThemeContext>
  )
}

function Form() {
  return (
    <Panel title="欢迎">
      <Button>注册</Button>
      <Button>登录</Button>
      <ThemeContext value="light">
        <Footer />
      </ThemeContext>
    </Panel>
  );
}

function Footer() {
  return (
    <footer>
      <Button>设置</Button>
    </footer>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      {title && <h1>{title}</h1>}
      {children}
    </section>
  )
}

function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}


在传递对象和函数时优化重新渲染

你可以通过 context 传递任何值,包括对象和函数。

function MyApp() {
const [currentUser, setCurrentUser] = useState(null);

function login(response) {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}

return (
<AuthContext value={{ currentUser, login }}>
<Page />
</AuthContext>
);
}

这里,context 值 是一个包含两个属性的 JavaScript 对象,其中一个属性是函数。每当 MyApp 重新渲染时(例如路由更新时),这都会变成一个指向不同函数的不同对象,因此 React 也必须重新渲染树中深处所有调用 useContext(AuthContext) 的组件。

在较小的应用中,这不是问题。不过,如果底层数据,例如 currentUser,没有变化,就没有必要让它们重新渲染。为了帮助 React 利用这一点,你可以用 useCallback 包裹 login 函数,并用 useMemo 包裹对象创建。这是一种性能优化:

import { useCallback, useMemo } from 'react';

function MyApp() {
const [currentUser, setCurrentUser] = useState(null);

const login = useCallback((response) => {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}, []);

const contextValue = useMemo(() => ({
currentUser,
login
}), [currentUser, login]);

return (
<AuthContext value={contextValue}>
<Page />
</AuthContext>
);
}

由于这个改动,即使 MyApp 需要重新渲染,调用 useContext(AuthContext) 的组件也不必重新渲染,除非 currentUser 发生了变化。

了解更多关于 useMemouseCallback 的内容。


故障排除

我的组件没有看到来自提供者的值

这可能由以下几个常见原因导致:

  1. 你在调用 useContext() 的同一个组件中(或其下方)渲染了 <SomeContext>。请将 <SomeContext> 移到调用 useContext() 的组件之上并置于其外部。
  2. 你可能忘记用 <SomeContext> 包裹你的组件了,或者你可能把它放在了你以为的树结构中的不同位置。请使用 React DevTools. 检查层级是否正确。
  3. 你可能遇到了工具链中的某个构建问题,导致提供者组件看到的 SomeContext 和读取组件看到的 SomeContext 是两个不同的对象。例如,如果你使用了符号链接,就可能发生这种情况。你可以通过将它们分别赋值给 window.SomeContext1window.SomeContext2,然后在控制台中检查 window.SomeContext1 === window.SomeContext2 来验证。如果它们不相同,请在构建工具层面修复该问题。

即使默认值不同,我从上下文中始终得到 undefined

你的树中可能存在一个没有 value 的提供者:

// 🚩 无法工作:没有 value 属性
<ThemeContext>
<Button />
</ThemeContext>

如果你忘记指定 value,就相当于传入了 value={undefined}

你也可能是误用了不同的属性名:

// 🚩 无法工作:属性应该叫 "value"
<ThemeContext theme={theme}>
<Button />
</ThemeContext>

在这两种情况下,你都应该在控制台中看到来自 React 的警告。要修复它们,请将属性命名为 value

// ✅ 传递 value 属性
<ThemeContext value={theme}>
<Button />
</ThemeContext>

请注意,createContext(defaultValue) 调用中的默认值 只有在上方完全没有匹配的提供者时才会使用。 如果父级树中的某处存在 <SomeContext value={undefined}> 组件,那么调用 useContext(SomeContext) 的组件将会收到 undefined 作为上下文值。