Context 解决的是什么问题
在 React 的数据流里,props 是父组件向子组件传递数据的唯一方式。当一个数据需要传递多层——从顶层组件传到第五层子组件——但中间层组件并不需要这个数据,只是充当传递的管道,这时候 props 传递就变得很麻烦。
这就是 Context 要解决的问题:跨越中间层级直接传递数据,而不需要每层手动透传 props。
一个经典的例子是主题功能:
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
// 不需要 props 透传
return <ThemedButton />;
}
function ThemedButton() {
// 直接从 Context 读取主题值
const theme = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}
Toolbar 组件不需要知道主题是什么,也不需要把 theme 作为 props 传下去。ThemedButton 直接从 Context 拿到主题值。这就是 Context 的核心价值:避免了不必要的 props 透传。
Context 的工作机制
createContext 创建的不是一个响应式的数据绑定,而是一个容器。Context 本身不存储值,值存储在 Provider 组件里。
const ThemeContext = createContext('light');
这行代码创建了两个组件:ThemeContext.Provider 和 ThemeContext.Consumer。在 Hooks 出现之前,读取 Context 的唯一方式是 Consumer 组件。useContext Hook 让读取 Context 变得更简单。
当组件调用 useContext(ThemeContext) 时,React 会在组件树向上查找最近的 ThemeContext.Provider,然后读取它的 value。
关键点:Context 的 value 变化时,所有使用这个 Context 的组件都会重新渲染。这意味着 Context 不适合存储频繁变化的状态——每次 value 变化都会触发所有消费者的重新渲染。
Context 和重新渲染的关系
看一个具体的例子:
function App() {
const [user, setUser] = useState({ name: 'Tom', age: 25 });
const [theme, setTheme] = useState('dark');
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<Dashboard />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
当 theme 变化时,使用 ThemeContext 的所有组件都会重新渲染。但如果 user 对象引用变化了(比如 setUser({ name: 'Tom', age: 25 }) 创建了一个新对象),使用 UserContext 的组件也会重新渲染。
这就引出一个重要的性能问题:如果 value 是一个对象或数组,每次 Provider 重新渲染都会创建新的引用,导致所有消费者重新渲染。
解决方式是让 Provider 的 value 尽量稳定:
// 不好:每次渲染都创建新对象
<UserContext.Provider value={{ name: user.name, age: user.age }}>
// 好:稳定引用
<UserContext.Provider value={user}>
或者把 context 拆分成更细的粒度,减少每次变化影响的组件范围。
状态管理的选择
React 本身提供了三种状态管理方式:
组件内部 state(useState、useReducer):适合组件自己使用的状态,不需要和其他组件共享。
Props 透传:适合在父组件和直接子组件之间传递数据,层级不超过两层的情况。
Context:适合在组件树的某个分支内共享数据,但不适合频繁变化的状态。
当这三种方式都不够用时,就需要引入外部状态管理方案。
什么时候该用 Context,什么时候该用 Redux
这是一个经常被问到的问题,但其实答案是很清晰的。
用 Context 的场景:
- 数据不经常变化(如主题、语言偏好、用户登录状态)
- 需要在整个组件树或某个分支内共享
- 数据流相对简单,不需要复杂的状态转换逻辑
用 Redux(或类似方案)的场景:
- 状态需要跨多个不相关的组件共享
- 状态变化逻辑复杂(需要 middleware、需要日志、需要持久化)
- 需要调试工具(Redux DevTools 能看到状态变化的完整历史)
- 团队对状态管理有统一的规范要求
Dan Abramov(Redux 的作者)在 2018 年的一篇文章里说过:"If you're not sure whether you need Redux, you probably don't need Redux." 这句话到今天仍然适用。
状态管理的分层设计
在实际项目中,状态管理通常需要分层设计:
第一层:组件本地状态。UI 状态(如弹窗开关、折叠状态)应该留在组件内部,不需要提升到 Context 或 Redux。
第二层:Context 或状态管理器。跨组件共享的数据,使用 Context(简单场景)或 Redux/Zustand/Jotai(复杂场景)。
第三层:服务端状态。来自 API 的数据,使用 React Query、SWR 或类似的库来管理缓存和同步。
服务端状态是一个经常被忽视的独立问题:它不仅仅是"存储数据",还包括缓存、加载状态、错误处理、乐观更新等。专门的服务端状态管理库能大大简化这类逻辑。
常见的状态管理错误
错误一:把所有状态都放进 Redux。这不是最佳实践。Redux 的 boilerplate 较多,频繁变化的小状态放在组件本地更合适。
错误二:状态放置层级过深。如果一个 Context Provider 的 value 频繁变化,导致大量组件重新渲染,考虑把 Context 拆细,或者把变化频繁的状态留在子组件内部。
错误三:滥用 Context 做事件传递。Context 是用来传递数据的,不是用来传递事件的。如果想在深层组件触发父组件的行为,应该用回调函数通过 props 传递,或者用 useReducer + useContext 的组合拳。
这一章想说的
Context 解决的是 props 透传问题,适合存储不经常变化的数据。它的核心机制是:Provider 存储 value,消费者通过 useContext 读取,value 变化时所有消费者重新渲染。
状态管理需要分层设计:组件本地 state、Context 或状态管理器、服务端状态,各自处理不同类型的数据。不要把所有状态都往 Redux 里塞,也不要因为 Redux 有调试工具就默认用它。
理解每种状态管理方式的特点和适用场景,才能在真实项目中做出正确的架构决策。