Skip to content

useContext

基本使用

介绍

useContext 是一个 React Hook,用于在组件中访问上下文。上下文是一个组件树中的变量,它可以在组件树中传递给任何组件。

示例

  1. 使用 createContext 方法创建一个上下文对象 MyContext
  2. 在顶层组件中通过 MyContext.Provider 组件提供数据。
  3. 在底层组件中通过 useContext 钩子函数获取消费数据。
jsx
import { createContext, useContext, useState } from 'react'

const MyContext = createContext({
  count: 0,
  setCount: (value) => {},
})

const Child = () => {
  const { count, setCount } = useContext(MyContext)

  return (
    <>
      <h3>Child {count}</h3>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </>
  )
}

const Parent = () => {
  const { count } = useContext(MyContext)

  return (
    <>
      <h2>Parent {count}</h2>
      <Child />
    </>
  )
}

const App = () => {
  const [count, setCount] = useState(0)

  return (
    <>
      <MyContext.Provider value={{ count, setCount }}>
        <h1>App {count}</h1>
        <Parent />
      </MyContext.Provider>
    </>
  )
}

export default App

useContext 的性能陷阱

useContext 的核心机制是“广播”:当 Provider 的 value 改变时,所有消费该 Context 的组件都会被重新渲染。

问题的根源:即使组件只依赖于 value 对象中的一小部分数据,只要 value 对象的引用发生了变化,该组件就会被强制更新。这就是不必要渲染的主要来源。

1. 拆分 Context

将一个大的 Context 拆分成多个独立的、更小的 Context。

jsx
const GlobalContext = createContext(null)

function App() {
  const [theme, setTheme] = useState('light')
  const [user, setUser] = useState(null)

  const value = useMemo(() => ({ theme, setTheme, user, setUser }), [theme, user])

  return (
    <GlobalContext.Provider value={value}>
      <ThemeToggleButton />
      <UserProfile />
    </GlobalContext.Provider>
  )
}
jsx
// 1. 创建独立的 Context
const ThemeContext = createContext(null)
const UserContext = createContext(null)

// 2. 在顶层组件中分别提供不同的 Context
function App() {
  const [theme, setTheme] = useState('light')
  const [user, setUser] = useState(null)

  // 保证 value 引用的稳定性
  const themeValue = useMemo(() => ({ theme, setTheme }), [theme])
  const userValue = useMemo(() => ({ user, setUser }), [user])

  return (
    <>
      <ThemeContext.Provider value={themeValue}>
        <ThemeToggleButton />
      </ThemeContext.Provider>
      <UserContext.Provider value={userValue}>
        <UserProfile />
      </UserContext.Provider>
    </>
  )
}

// 3. 组件按需消费
function ThemeToggleButton() {
  const { theme, setTheme } = useContext(ThemeContext) // 只订阅主题
  // ...
}

function UserProfile() {
  const { user, setUser } = useContext(UserContext) // 只订阅用户
  // ...
}

2. 利用组合与 children Prop

将不依赖 Context 变化的组件(尤其是渲染昂贵的组件)作为 children 传入 Provider,从而将它们“隔离”在渲染循环之外。

利用了 React 的渲染机制:如果一个组件的 props(包括 children)引用没有变化,React 会更倾向于跳过对它的更新。

jsx
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  const themeValue = useMemo(() => ({ theme, setTheme }), [theme])

  return (
    <ThemeContext.Provider value={themeValue}>
      <ThemeToggleButton /> {/* 这个组件依赖于 theme */}
      {children} {/* 引用稳定 */}
    </ThemeContext.Provider>
  )
}

function App() {
  return (
    <ThemeProvider>
      {/* 作为 children 传入,不会因为 theme 的变化而重新渲染 */}
      <ExpansiveStaticComponent />
    </ThemeProvider>
  )
}

3. 分离数据与 API

将易变的状态(state)和稳定的更新函数(dispatch)分离到不同的 Context 中。

jsx
// 1. 创建两个 Context
const CountStateContext = createContext(null)
const CountDispatchContext = createContext(null)

// 2. 在 Provider 中分别提供
function CountProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    <CountStateContext.Provider value={state}>
      <CountDispatchContext.Provider value={dispatch}>{children}</CountDispatchContext.Provider>
    </CountStateContext.Provider>
  )
}

// 3. 只读取状态
const state = useContext(CountStateContext)

// 4. 只读取更新函数(不会因 state 变化而重新渲染)
const dispatch = useContext(CountDispatchContext)

useContext 配合 useReducer 使用

TODO

基于 MIT 许可发布