useContext 读取父组件提供的值,这样组件树深处的组件也能拿到它,而无需中间的每个组件都把它作为 prop 传下去。你用 createContext 创建一个 context,用带 value 的 provider 包裹组件树的一部分,然后在任何需要这个值的地方调用 useContext(TheContext)。
Toolbar 从来没有提到主题,但勾选复选框时,两个按钮都会改变。这正是 context 的意义所在:中间的组件不必携带自己用不到的 prop,这就是 context 所解决的 prop drilling 问题。
三个步骤
// 1. Create it once, outside any component, usually in its own file
export const ThemeContext = createContext('light');
// 2. Provide a value to a part of the tree
<ThemeContext value={theme}>
<Page />
</ThemeContext>
// 3. Read it in any component inside that part
const theme = useContext(ThemeContext);
在 React 19 中,context 对象本身就是 provider。较老的代码写作 <ThemeContext.Provider value={theme}>,这种写法仍然有效,所以两种你都会见到。
useContext 会沿着组件树向上查找该 context 最近的 provider。组件读取的是离它最近的那个,所以你可以嵌套 provider,为某一部分覆盖原来的值。
默认值
createContext 的参数,就是组件上方没有 provider 时 useContext 返回的值。它不会随 state 变化而变化;它只是一个后备值。
第一个 Greeting 上方没有 provider,所以拿到默认值 'en'。第二个读取到 'es',第三个读取到离它更近的 'ja'。把外层的 value="es" 改成 value="ja",只有第二行会改变。
在子组件中更新 context
context 只负责把值往下传。想让深层的子组件修改它,就在 provider 所在的地方把这个值存为 state,再把 setter 和值一起放进 context。
LoginButton 调用 setUser,App 中的 state 随之改变,provider 得到一个新的值,Header 也跟着更新。两个组件都没有接收任何 prop。
值变化时,每个使用者都会重新渲染
当 provider 的 value 变化时,React 会渲染每一个为它调用 useContext 的组件,即使这个组件位于 memo 之内、props 也没有变。React 用 Object.is 比较新旧值。所以 value={{ user, setUser }} 在 App 每次渲染时都是一个新对象,即使 user 没有变,每个使用者也都会重新渲染。
第一次渲染时,两个读取组件都会各打印一次。点击按钮:只有 PlainReader 再次打印,因为它的 context 拿到了一个新对象。useMemo 在 name 变化之前一直把同一个对象交给 MemoContext,所以 MemoReader 保持不动。只有当渲染变慢时,这一点才重要;对于小型应用,每次渲染生成一个新对象完全没问题。
拆分 context
经常变化的值和从不变化的值不应该共用一个 context。如果 setter 和 state 放在一起,一个只需要调用 setUser 的组件,也会在 user 每次变化时重新渲染。把它们放进两个 context,每个使用者就只订阅自己读取的内容:
const UserContext = createContext(null);
const SetUserContext = createContext(() => {});
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<SetUserContext value={setUser}>
<UserContext value={user}>{children}</UserContext>
</SetUserContext>
);
}
useState 返回的 setUser 永远保持同一个身份,所以只读取 SetUserContext 的组件永远不会因为登录而重新渲染。同样的思路也适用于互不相关的数据:主题和购物车应该放在不同的 context 里,而不是一个大大的 AppContext。
常见的收尾做法是写一个自定义 Hook,比如 useUser(),由它调用 useContext(UserContext),并在缺少 provider 时抛出一条清楚的错误,这样调用方就永远不需要直接导入 context 对象。
context、props 还是状态管理库
- props 是默认选择。把一个值往下传两三层,清楚又容易追踪。
- context 适合许多组件在不同层级都需要、而且很少变化的值:主题、语言、当前登录的用户、功能开关对象。
- 状态管理库(Redux Toolkit、Zustand、Jotai 等)适合体量大、更新频繁的 state,此时你希望组件只订阅其中一部分,跳过由其他部分引起的渲染。context 没有这样的选择器:值的任何变化都会让使用者重新渲染。
在动用 context 或库之前,先看看把组件作为 children 传入能不能消除层层传递。很多时候可以,而且根本不需要任何新 API。
常见问题
React 中的 useContext 有什么作用?
它返回一个 context 的当前值,这个值来自组件树中该组件上方最近的 provider。当这个 provider 的值变化时,每个读取它的组件都会重新渲染。
createContext 中的默认值有什么用?
当组件上方没有 provider 时,useContext 返回的就是它。它适用于测试和单独渲染的组件,而且永远不会改变。
React 19 中还需要 Context.Provider 吗?
不需要。在 React 19 中,你可以直接把 context 本身当作 provider 渲染:<ThemeContext value="dark">。<ThemeContext.Provider value="dark"> 仍然有效,所以旧代码可以继续运行。
如何在子组件中更新 context 的值?
在渲染 provider 的组件中把这个值存为 state,再把 setter 和值一起放进 context:value={{ theme, setTheme }}。子组件用 useContext 读取 setTheme 并调用它。
React context 能替代 Redux 吗?
context 负责把一个值沿组件树往下传;它本身并不管理 state。配合 useState 或 useReducer,它能满足很多应用的需要。状态管理库则补上了 context 缺少的东西,比如只订阅某一部分数据,让无关的更新不会让组件重新渲染,还有开发者工具和中间件。