useContext

useContext 是一个 React Hook,允许你读取和订阅组件中的上下文

const value = useContext(SomeContext)

参考

useContext(SomeContext)

在组件的顶层调用useContext 来读取和订阅上下文

import { useContext } from 'react';

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

请参见下面的更多示例。

参数

  • SomeContext:你之前用createContext创建的上下文。上下文本身并不保存信息,它只代表你可以从组件中提供或读取的信息类型。

返回值

useContext 返回调用组件的上下文值。它被确定为传递给树中调用组件上方最近的SomeContext.Providervalue。如果没有这样的提供者,则返回值将是你传递给createContext的该上下文的defaultValue。返回值始终是最新的。如果上下文发生更改,React 会自动重新渲染读取该上下文的组件。

注意事项

  • 组件中的useContext() 调用不受来自相同组件的提供者影响。<Context.Provider> 必须位于进行useContext()调用的组件上方
  • React 会自动重新渲染所有使用特定上下文的孩子组件,重新渲染起始于接收到不同value 的提供者。之前的和下一个值会使用Object.is 进行比较。使用memo 跳过重新渲染不会阻止子组件接收新的上下文值。
  • 如果你的构建系统在输出中产生了重复的模块(这在使用符号链接时可能会发生),这可能会破坏上下文。只有当你在提供上下文时使用的SomeContext 和你在读取上下文时使用的SomeContext完全相同的对象(由=== 比较确定)时,通过上下文传递数据才能正常工作。

用法

向树中更深层地传递数据

在组件的顶层调用useContext 来读取和订阅上下文

import { useContext } from 'react';

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

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

要将上下文传递给Button,请将其或其父组件之一包装到相应的上下文提供者中。

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

function Form() {
// ... renders buttons inside ...
}

提供者和Button 之间有多少层组件并不重要。当Form 内部的任何位置的Button 调用useContext(ThemeContext) 时,它将接收"dark" 作为值。

陷阱

useContext() 总是查找调用它的组件上方最近的提供者。它向上搜索,并且不会考虑你正在从中调用useContext() 的组件中的提供者。

import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

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

function Form() {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</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>
  );
}


更新通过上下文传递的数据

通常,你会希望上下文随着时间的推移而改变。要更新上下文,请将其与状态结合起来。在父组件中声明一个状态变量,并将当前状态作为上下文值传递给提供者。

function MyPage() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Form />
<Button onClick={() => {
setTheme('light');
}}>
Switch to light theme
</Button>
</ThemeContext.Provider>
);
}

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

更新上下文的示例

示例 1 5:
通过上下文更新值

在这个例子中,MyApp 组件包含一个状态变量,然后将其传递给ThemeContext 提供者。选中“暗模式”复选框会更新状态。更改提供的值会重新渲染所有使用该上下文的组件。

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

const ThemeContext = createContext(null);

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

function Form({ children }) {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</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} 传递的是 JavaScript theme 变量的值,并使用了JSX 花括号。花括号还可以让你传递不是字符串的上下文值。


指定回退默认值

如果 React 在父级树中找不到任何特定上下文 的提供者,则 useContext() 返回的上下文值将等于你在创建该上下文时指定的默认值

const ThemeContext = createContext(null);

默认值永远不会改变。如果要更新上下文,请按照上面所述将其与状态一起使用。

通常,除了null,还可以使用一些更有意义的值作为默认值,例如

const ThemeContext = createContext('light');

这样,即使意外地渲染了没有相应提供者的组件,也不会中断。这也有助于你的组件在测试环境中良好运行,而无需在测试中设置大量提供者。

在下面的示例中,“切换主题”按钮始终为浅色,因为它位于任何主题上下文提供者之外,并且默认上下文主题值为'light'。尝试将默认主题编辑为'dark'

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

const ThemeContext = createContext('light');

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

function Form({ children }) {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</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>
  );
}


覆盖树的一部分的上下文

可以通过使用具有不同值的提供者包装该部分来覆盖树的一部分的上下文。

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

可以根据需要嵌套和覆盖提供者。

上下文覆盖的示例

示例 1 2:
覆盖主题

这里,位于Footer内部的按钮接收到的上下文值("light")与外部按钮("dark")不同。

import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

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

function Form() {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
      <ThemeContext.Provider value="light">
        <Footer />
      </ThemeContext.Provider>
    </Panel>
  );
}

function Footer() {
  return (
    <footer>
      <Button>Settings</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>
  );
}


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

可以通过上下文传递任何值,包括对象和函数。

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

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

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

这里,上下文值 是一个具有两个属性的 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.Provider value={contextValue}>
<Page />
</AuthContext.Provider>
);
}

由于进行了此更改,即使MyApp需要重新渲染,调用useContext(AuthContext)的组件也不需要重新渲染,除非currentUser已更改。

阅读有关useMemouseCallback 的更多信息。


故障排除

我的组件看不到我的提供者的值

出现这种情况有几种常见原因

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

尽管默认值不同,但我总是从我的上下文获取undefined

您的树中可能缺少value属性的提供程序。

// 🚩 Doesn't work: no value prop
<ThemeContext.Provider>
<Button />
</ThemeContext.Provider>

如果您忘记指定value,这就像传递value={undefined}一样。

您可能还错误地使用了不同的属性名称。

// 🚩 Doesn't work: prop should be called "value"
<ThemeContext.Provider theme={theme}>
<Button />
</ThemeContext.Provider>

在这两种情况下,您都应该在控制台中看到 React 的警告。要修复它们,请调用属性value

// ✅ Passing the value prop
<ThemeContext.Provider value={theme}>
<Button />
</ThemeContext.Provider>

请注意,来自createContext(defaultValue)调用的默认值仅在根本没有匹配的提供程序位于其上方时才使用。如果父级树中的某个位置存在<SomeContext.Provider value={undefined}>组件,则调用useContext(SomeContext)的组件将接收undefined作为上下文值。