React Context 详解:从入门到性能优化
2026年3月3日 14:21
React Context 详解:从入门到性能优化
本文适合熟悉 Vue 但刚开始学习 React 的开发者,通过 Vue 的
provide/inject对比来理解 React Context。
一、什么是 Context?
在组件开发中,我们经常遇到这样的场景:某个数据需要在多层嵌套的组件间共享。如果一层层通过 props 传递,代码会变得非常冗长且难以维护,这就是所谓的 "prop drilling" 问题。
React 的 Context 和 Vue 的 provide/inject 都是为了解决这个问题而设计的 —— 它们允许数据跨层级传递,跳过中间组件。
二、React Context 基础用法
核心三步
- 创建 Context —— 创建一个数据共享的"通道"
- 提供数据 —— 父组件通过 Provider 提供数据
- 消费数据 —— 子组件通过 useContext 获取数据
完整示例
// ========== 1. 创建 Context ==========
// context.tsx
import { createContext, useContext } from 'react'
// 定义数据类型
type MyContextValue = {
name: string
age: number
}
// 创建 Context(可设置默认值)
const MyContext = createContext<MyContextValue>({ name: '', age: 0 })
// 导出一个 hook 方便使用
const useMyContext = () => useContext(MyContext)
export { MyContext, useMyContext }
// ========== 2. 父组件提供数据 ==========
// parent.tsx
import { MyContext } from './context'
import Child from './child'
const Parent = () => {
const data = { name: '张三', age: 18 }
return (
<MyContext.Provider value={data}>
<Child />
</MyContext.Provider>
)
}
// ========== 3. 子组件消费数据 ==========
// child.tsx
import { useMyContext } from './context'
const Child = () => {
const { name, age } = useMyContext()
return <div>{name} - {age}岁</div>
}
三、对比 Vue 的 provide/inject
如果你熟悉 Vue,这个概念其实非常相似:
| 步骤 | React | Vue |
|---|---|---|
| 创建 | createContext() |
无需显式创建 |
| 提供 | <Context.Provider value={}> |
provide(key, value) |
| 消费 | useContext(Context) |
inject(key) |
Vue 等价写法
<!-- 父组件 -->
<script setup>
import { provide } from 'vue'
import Child from './child.vue'
const data = { name: '张三', age: 18 }
provide('myContext', data)
</script>
<template>
<Child />
</template>
<!-- 子组件 -->
<script setup>
import { inject } from 'vue'
const { name, age } = inject('myContext')
</script>
<template>
<div>{{ name }} - {{ age }}岁</div>
</template>
可以看到,两者的设计思想是一致的,只是语法不同:
- React 使用 JSX 的组件包裹方式
<Context.Provider> - Vue 使用 Composition API 的函数调用方式
四、原生 Context 的性能问题
原生 React Context 存在一个性能陷阱:
只要 Context value 中的任何一个字段变化,所有消费这个 Context 的组件都会重新渲染,即使它们只用到了没变的字段。
// 原生 React Context 的问题
const MyContext = createContext({ name: '张三', age: 18, city: '北京' })
// 这个组件只用 name,但 age 或 city 变化时也会重新渲染!
const Child = () => {
const { name } = useContext(MyContext)
return <div>{name}</div>
}
当 Context 中有几十个字段时(这在大型应用中很常见),这个问题会严重影响性能。
五、use-context-selector:性能优化方案
为了解决这个问题,社区提供了 use-context-selector 库。它支持选择器模式,让组件只订阅自己关心的字段。
安装
npm install use-context-selector
使用方式
// 从 use-context-selector 导入,而不是 react
import { createContext, useContext } from 'use-context-selector'
const MyContext = createContext({ name: '张三', age: 18, city: '北京' })
// 使用选择器,只订阅 name
const Child = () => {
const name = useContext(MyContext, v => v.name) // age 或 city 变化不会触发重渲染
return <div>{name}</div>
}
核心区别
| 特性 | React 原生 | use-context-selector |
|---|---|---|
| 导入来源 | 'react' |
'use-context-selector' |
| 更新粒度 | 整个 Context 变化就重渲染 | 可以用选择器精确订阅某个字段 |
| 性能 | 大型 Context 可能性能差 | 优化了选择器模式,避免不必要的重渲染 |
| 使用方式 | useContext(ctx) |
useContext(ctx, selector?) |
六、实际案例分析
以 Dify 项目中的 ChatWithHistoryContext 为例:
// context.tsx
import { createContext, useContext } from 'use-context-selector'
export type ChatWithHistoryContextValue = {
appMeta?: AppMeta | null
appData?: AppData | null
appParams?: ChatConfig
currentConversationId: string
conversationList: AppConversationData['data']
handleNewConversation: () => void
handleChangeConversation: (conversationId: string) => void
// ... 还有 20+ 个字段
}
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
currentConversationId: '',
// ... 默认值
})
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)
// parent.tsx - 提供数据
const ChatWithHistoryWrap = () => {
const contextValue = useChatWithHistory() // 获取所有数据
return (
<ChatWithHistoryContext.Provider value={contextValue}>
<ChatWithHistory />
</ChatWithHistoryContext.Provider>
)
}
// child.tsx - 消费数据
const ChatWithHistory = () => {
const {
appData,
conversationList,
handleChangeConversation
} = useChatWithHistoryContext()
// 使用数据...
}
这个 Context 有 30+ 个字段,如果使用原生 Context,任何一个字段变化都会导致所有子组件重渲染。使用 use-context-selector 后,框架内部做了优化,避免了不必要的渲染。
七、数据流图解
┌─────────────────────────────────────────────────┐
│ ChatWithHistoryWrap (父组件) │
│ │
│ 通过 useChatWithHistory() 获取所有数据 │
│ { appData, appParams, conversationList, ... } │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ ChatWithHistoryContext.Provider │
│ value={{ appData, appParams, ... }} │ ← 数据注入到 Context
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ ChatWithHistory (子组件) │
│ │
│ useChatWithHistoryContext() 获取数据 │
└─────────────────────────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Sidebar │ │ Header │ │ ChatWrap│
└─────────┘ └─────────┘ └─────────┘
│ │ │
└─────────────┴─────────────┘
│
孙组件同样可以通过
useChatWithHistoryContext() 获取数据
八、最佳实践
- 小型项目:使用原生 React Context 即可,简单直接
-
大型项目:当 Context 字段较多(10+)时,考虑使用
use-context-selector - 拆分 Context:如果可能,将不相关的数据拆分到不同的 Context 中
-
命名规范:导出一个自定义 hook(如
useMyContext),统一消费方式
九、总结
| 场景 | 推荐方案 |
|---|---|
| 简单数据共享 | React 原生 Context |
| 大型 Context,字段多 | use-context-selector |
| Vue 背景开发者 | 理解为 provide/inject 的 React 版本 |
Context 本质上就是 跨层级传递数据 的工具,理解了这一点,无论是 React 还是 Vue,核心概念都是相通的。