1. 为什么需要 Context?(解决什么问题)
在 React 中,数据通常是自上而下通过 props 传递的,这被称为"prop drilling"(属性钻取)。 想象一下这个场景:你的应用有一个主题(比如 light 或 dark),这个主题需要在很多深层级的组件中使用。
jsx
// App.js
function App() {
const theme = 'dark';
return <Layout theme={theme} />;
}
// Layout.js
function Layout({ theme }) {
return <Header theme={theme} />;
}
// Header.js
function Header({ theme }) {
return <UserAvatar theme={theme} />;
}
// UserAvatar.js
function UserAvatar({ theme }) {
// 终于用到了!但为了它,theme 穿透了三层不相关的组件。
return <div className={theme}>Avatar</div>;
}
这种方式的缺点很明显:
- 代码冗余 :中间层组件(
Layout,Header)被迫接收并传递它们自己并不使用的props。 - 难以维护 :如果需要传递一个新的属性,或者修改属性名,你必须修改所有中间层的组件。 Context 就是解决这个问题的方案 。它提供了一种在组件树中跨层级共享数据的方式,无需手动地逐层传递
props。
2. Context 的核心 API 与使用步骤
使用 Context 主要分为三步:
- 创建 Context 对象
- 提供 Context 值
- 消费 Context 值 我们用上面的"主题切换"例子来完整演示。
步骤一:创建 Context
使用 React.createContext 创建一个 Context 对象。这个对象包含两个组件:Provider 和 Consumer。
jsx
// ThemeContext.js
import React from 'react';
// 创建一个 Context 对象,并设置一个默认值 'light'
// 当一个组件消费该 Context,但没有找到对应的 Provider 时,就会使用这个默认值。
export const ThemeContext = React.createContext('light');
步骤二:提供 Context 值
使用 ThemeContext.Provider 包裹组件树,并通过 value 属性提供要共享的值。所有被它包裹的后代组件都可以访问到这个值。
jsx
// App.js
import React, { useState } from 'react';
import { ThemeContext } from './ThemeContext';
import Toolbar from './Toolbar';
function App() {
// 使用 state 来管理主题,这样就可以动态切换
const [theme, setTheme] = useState('dark');
// Provider 的 value 属性就是我们要共享的值
return (
<ThemeContext.Provider value={theme}>
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle Theme
</button>
<Toolbar />
</ThemeContext.Provider>
);
}
export default App;
关键点:
Provider的value属性是必须的,它决定了后代组件能获取到什么值。value的值可以是任何类型:字符串、数字、对象、函数等。
步骤三:消费 Context 值
后代组件有两种主要方式来消费 Context 的值。
方式 A:useContext Hook (推荐,最常用)
这是在函数组件中最简洁、最流行的方式。
jsx
// ThemedButton.js
import React, { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedButton() {
// 使用 useContext Hook,传入 Context 对象
// 它会返回离它最近的 Provider 的 value 值
const theme = useContext(ThemeContext);
// 根据 theme 值动态设置样式
const style = {
background: theme === 'dark' ? '#333' : '#FFF',
color: theme === 'dark' ? 'white' : 'black',
padding: '10px 20px',
border: '1px solid',
};
return <button style={style}>I am a {theme} button</button>;
}
export default ThemedButton;
方式 B:Context.Consumer (旧版语法,用于类组件)
在 useContext Hook 出现之前,这是在函数组件和类组件中消费 Context 的标准方式。现在主要用于类组件。
jsx
// ThemedButtonLegacy.js
import React from 'react';
import { ThemeContext } from './ThemeContext';
class ThemedButtonLegacy extends React.Component {
render() {
return (
// Consumer 使用 render props 模式
// 它需要一个函数作为子元素,这个函数会接收到 context 的 value
<ThemeContext.Consumer>
{theme => (
<button style={{
background: theme === 'dark' ? '#333' : '#FFF',
color: theme === 'dark' ? 'white' : 'black',
}}>
I am a {theme} button (Legacy)
</button>
)}
</ThemeContext.Consumer>
);
}
}
export default ThemedButtonLegacy;
3. 完整示例代码结构
为了让你看得更清楚,这里是一个完整的文件结构:
scss
src/
├── App.js // 提供 Context 的 Provider
├── ThemeContext.js // 创建并导出 Context
├── Toolbar.js // 中间层组件,不关心 theme
└── ThemedButton.js // 消费 Context 的组件
ThemeContext.js
jsx
import React from 'react';
export const ThemeContext = React.createContext('light');
App.js
jsx
import React, { useState } from 'react';
import { ThemeContext } from './ThemeContext';
import Toolbar from './Toolbar';
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle Theme
</button>
<Toolbar />
</ThemeContext.Provider>
);
}
export default App;
Toolbar.js
jsx
import React from 'react';
import ThemedButton from './ThemedButton';
function Toolbar() {
// 它不需要知道 theme,直接渲染子组件即可
return (
<div>
<h1>Toolbar</h1>
<ThemedButton />
</div>
);
}
export default Toolbar;
ThemedButton.js
jsx
import React, { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedButton() {
const theme = useContext(ThemeContext);
const style = {
background: theme === 'dark' ? '#333' : '#FFF',
color: theme === 'dark' ? 'white' : 'black',
padding: '10px 20px',
border: '1px solid',
};
return <button style={style}>I am a {theme} button</button>;
}
export default ThemedButton;
4. 最佳实践与注意事项
-
将 Provider 和 Consumer 分离
- 通常,我们会把
createContext的结果放在一个单独的文件中(如ThemeContext.js),这样方便在应用的不同地方导入和使用。
- 通常,我们会把
-
避免滥用 Context
- Context 主要用于**"全局"**数据,比如主题、用户信息、语言偏好等。
- 如果只是两三个层级的组件树需要共享数据,使用
props传递可能更简单、更清晰。滥用 Context 会让组件的复用变得困难,因为组件隐式地依赖了外部的 Context。
-
Provider 的
value值变化会触发所有消费者重渲染- 当
Provider的value值发生变化时,所有消费该 Context 的组件都会重新渲染。 - 性能陷阱 :如果
value是一个对象或数组,每次渲染都创建一个新的对象/数组,即使内容没变,也会导致消费者组件不必要的重渲染。 错误示范:
jsx<MyContext.Provider value={{ someValue: 'abc' }}> // 每次都创建新对象! <Children /> </MyContext.Provider>正确做法 :将
value的值提升到state或useMemo中。jsximport React, { useState, useMemo } from 'react'; function App() { const [user, setUser] = useState({ name: 'John' }); // 使用 useMemo 缓存对象,只有当 user 变化时才创建新对象 const contextValue = useMemo(() => ({ user, setUser }), [user]); return ( <UserContext.Provider value={contextValue}> <Profile /> </UserContext.Provider> ); } - 当
-
一个组件可以消费多个 Context
jsximport { useContext } from 'react'; import { ThemeContext, UserContext } from './contexts'; function Header() { const theme = useContext(ThemeContext); const user = useContext(UserContext); return <h1 style={{ color: theme.color }}>Welcome, {user.name}</h1>; }