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 传给 createContext 的 defaultValue。返回值始终是最新的。如果某个 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" 作为值。
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' 值重新渲染。
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>你可以根据需要多次嵌套并覆盖提供者。
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 发生了变化。
了解更多关于 useMemo 和 useCallback 的内容。
故障排除
我的组件没有看到来自提供者的值
这可能由以下几个常见原因导致:
- 你在调用
useContext()的同一个组件中(或其下方)渲染了<SomeContext>。请将<SomeContext>移到调用useContext()的组件之上并置于其外部。 - 你可能忘记用
<SomeContext>包裹你的组件了,或者你可能把它放在了你以为的树结构中的不同位置。请使用 React DevTools. 检查层级是否正确。 - 你可能遇到了工具链中的某个构建问题,导致提供者组件看到的
SomeContext和读取组件看到的SomeContext是两个不同的对象。例如,如果你使用了符号链接,就可能发生这种情况。你可以通过将它们分别赋值给window.SomeContext1和window.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 作为上下文值。