React useContext:跨层级共享数据的上下文

当组件树的深度让 props 传递变成一场"搬运接力",useContext 就是那个直达终点的电梯。

一、先看组件通信有多痛

一个页面里,组件之间的关系无非四种:父子、兄弟、爷孙、陌生人。

  • 父子:最直接,props 传数据,自定义事件传回调------单向数据流。
  • 兄弟 / 爷孙 / 陌生人 :它们之间没有直接通道。最朴素的做法是------把数据提到它们共同的父组件,然后用 props 一层一层往下传。

问题就出在这里。当层级变深,中间层的组件就成了纯粹的"搬运工":

css 复制代码
App
└── Layout
    └── Main
        └── Sidebar          ← 想要 theme,但数据在 App
            └── UserCard     ← 其实它只是路过

爷孙之间要传一个 theme,中间每一层都得:

jsx 复制代码
const Main = ({ theme }) => (
  <Sidebar theme={theme} />   // 我根本不用 theme,我只是个传话的
)

数据根本用不上,却要一层层地收、一层层地递,这种"搬运工作"就是 prop drilling(逐层钻透)。层级一深,全是样板代码,改一个字段名字,整条链路都要动。

二、useContext:数据源头提供,深层直接消费

useContext 的思路很朴素:在数据源头放一个"供给站",需要数据的组件直接去取,中间的层级完全不感知。

使用总共三步。

第 1 步:createContext 创建上下文

js 复制代码
// ThemeContext.jsx ------ 独立文件,作为公共资源被各处引用
import { createContext } from 'react'

export const ThemeContext = createContext("light")

注意 createContext("light") 里的 "light" 是默认值 。它的作用是兜底:当某个组件在组件树里没有被任何 Provider 包裹 时,useContext 就拿这个默认值,保证不崩、不报错。

第 2 步:Provider 提供数据

jsx 复制代码
// App.jsx
import { useState } from 'react'
import { ThemeContext } from './ThemeContext'
import Page from './Page'

const App = () => {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
      <button onClick={() => setTheme("dark")}>切换主题</button>
    </ThemeContext.Provider>
  )
}

两个关键点:

  1. value 绑定的不是常量,而是 state 。所以 Provider 提供的值是响应式 的------setTheme 一执行,所有消费方自动重新渲染。这是 context 和普通全局变量最大的区别。
  2. value 传的是一个对象/值 ,最好用 useMemo 包裹,否则父组件每次渲染,value 都会变,连带所有消费者一起重渲染(后面细说)。

第 3 步:深层组件直接消费

jsx 复制代码
// Page.jsx ------ 它下面还套着 Child
import { useContext } from 'react'
import { ThemeContext } from '../ThemeContext'

const Page = () => {
  const theme = useContext(ThemeContext)   // 直接读,不管自己离 App 有多远
  return <div>Page: {theme}</div>
}

Page 和 App 之间隔着多少层都无所谓,useContext 会向上查找最近的 Provider,拿到 theme。中间任何组件都不需要感知 props。

一个可运行的最小 Demo(主题切换):

jsx 复制代码
// App.jsx
const App = () => {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
      <button onClick={() => setTheme("dark")}>切换主题</button>
    </ThemeContext.Provider>
  )
}

// Page.jsx
const Page = () => {
  const theme = useContext(ThemeContext)
  return (
    <>
      <span>Page: {theme}</span>
      <Child />
    </>
  )
}

// Child.jsx ------ 深一层,照样直接读
const Child = () => {
  const theme = useContext(ThemeContext)
  return <button className={theme}>按钮 {theme}</button>
}

三、深入:Provider 不一定是"全局"的

很多人误以为 context 是全局的,其实 Context 数据只存在于 Provider 这个"容器"里,对容器内部的子树可见,容器外面拿不到。

Provider 像一根水管:从你放置的位置开始,只给自己的下游供水。

作用域:包住谁,谁才有资格拿

jsx 复制代码
<>
  <ThemeContext.Provider value={theme}>
    <Page />            {/* ✅ Page 和它内部的 Child 能看到 */}
  </ThemeContext.Provider>
  <button>切换主题</button>   {/* ❌ 在容器外,看不到,只能拿到默认值 "light" */}
</>

Provider 放在组件树的哪个位置,就决定了哪些组件能消费这份数据。这就是"不一定是全局的"------你可以只给一棵子树供水。

就近覆盖:同一个 Context 可以叠多个 Provider

这是它最灵活的地方:同一个 ThemeContext,可以在不同层级放不同的值,组件向上找最近的祖先 Provider。

ini 复制代码
App
└── Provider value="light"          ← 外层,兜底
    ├── Header                → 看到 light
    └── main
        ├── Provider value="dark"   ← 只覆盖这一支
        │   └── Sidebar       → 看到 dark(就近原则)
        └── Content           → 看到 light(穿透内层,落到外层)
jsx 复制代码
function App() {
  return (
    <ThemeContext.Provider value="light">
      <Header />
      <main>
        {/* 局部暗色区域:只影响这一支 */}
        <ThemeContext.Provider value="dark">
          <Sidebar />
        </ThemeContext.Provider>
        <Content />
      </main>
    </ThemeContext.Provider>
  )
}

Sidebar 找到内层的 "dark",Content 跳过内层、落到外层的 "light"。如果一路都没有 Provider,才用 createContext 时的默认值。

这个特性的实际价值:同一个 Context 可以同时服务多个互不干扰的区域 。比如 UserContext 顶部给整站提供登录用户,又可以在某个管理面板里用临时 Provider 覆盖成"管理员"视角,其余部分不受影响。

四、什么时候该用、什么时候别用

适合用 context 的:跨越多层、被多个组件共享、相对稳定的数据------主题、登录用户、语言 / 地区、购物车数量、UI 偏好。

不适合的:

  • 只有一层父子传值:直接 props 最直白,别为了一层也上 context。
  • 频繁变化 + 消费者很多 :Provider 的 value 一变,所有消费者整棵子树一起重渲染。高频更新(比如每秒变化的时间、实时坐标)如果被大量组件消费,会拖累性能。此时考虑拆细 Provider、给 value 包 useMemo,或上更专业的状态方案。

一句话总结:

Provider 像一个水管,从你放置的位置开始给自己的下游供水;每个 Provider 都是局部的一份,就近原则决定谁喝到哪一份。

相关推荐
鸿蒙开发1 小时前
Three.js 首帧卡顿排查:compileAsync 之后,为什么还会停一下?
react.js·three.js
小李科技9 小时前
【性能优化】事件委托:目标定位与 React/Vue 实战
前端·vue.js·react.js·面试·性能优化·前端框架
csj509 小时前
前端基础之《React(16)—Modal组件封装》
前端·react.js
ZoeLandia2 天前
React11:数据请求与 TanStack Query
前端·react.js·typescript·react
瓦瓦gogo2 天前
实习中接触前端开发后,我开始了解 React
前端·react.js·前端框架
ZoeLandia3 天前
React08:自定义 Hook
开发语言·前端·react.js·typescript
空中湖5 天前
用 React 写数据大屏:为什么我不建议用图表库
前端·react.js·前端框架
来一首_We_go6 天前
React Flow Pro 高级交互实现
前端·react.js
weedsfly6 天前
React 不可变数据:为什么页面“明明数据变了却不更新
前端·javascript·react.js
随风一样自由6 天前
【React+TS71007】Next.js “use client” 误用导致 TS71007:客户端/服务端引入问题的完整排查与修复
前端·javascript·react.js