React Context 详解:从入门到性能优化

28 阅读4分钟

React Context 详解:从入门到性能优化

本文适合熟悉 Vue 但刚开始学习 React 的开发者,通过 Vue 的 provide/inject 对比来理解 React Context。

一、什么是 Context?

在组件开发中,我们经常遇到这样的场景:某个数据需要在多层嵌套的组件间共享。如果一层层通过 props 传递,代码会变得非常冗长且难以维护,这就是所谓的 "prop drilling" 问题。

React 的 Context 和 Vue 的 provide/inject 都是为了解决这个问题而设计的 —— 它们允许数据跨层级传递,跳过中间组件。

二、React Context 基础用法

核心三步

  1. 创建 Context —— 创建一个数据共享的"通道"
  2. 提供数据 —— 父组件通过 Provider 提供数据
  3. 消费数据 —— 子组件通过 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,这个概念其实非常相似:

步骤ReactVue
创建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() 获取数据

八、最佳实践

  1. 小型项目:使用原生 React Context 即可,简单直接
  2. 大型项目:当 Context 字段较多(10+)时,考虑使用 use-context-selector
  3. 拆分 Context:如果可能,将不相关的数据拆分到不同的 Context 中
  4. 命名规范:导出一个自定义 hook(如 useMyContext),统一消费方式

九、总结

场景推荐方案
简单数据共享React 原生 Context
大型 Context,字段多use-context-selector
Vue 背景开发者理解为 provide/inject 的 React 版本

Context 本质上就是 跨层级传递数据 的工具,理解了这一点,无论是 React 还是 Vue,核心概念都是相通的。