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 都是局部的一份,就近原则决定谁喝到哪一份。

相关推荐
kill5221 天前
useRef
前端·javascript·react.js
西柚小萌新1 天前
【LLM&&AI应用开发 八股文】--4.2.Agent智能体(中)
前端·javascript·react.js
Java后端的Ai之路1 天前
03_React_JSX
前端·react.js·前端框架
晴天小庭2 天前
Sael——基于Jev的AI中转站的安全风控网关,现已开源
人工智能·后端·react.js
Java后端的Ai之路2 天前
01-React基础教程
开发语言·前端·python·react.js·前端框架
流水白开2 天前
React的Virtual DOM、Diff算法和Fiber
前端·react.js
光影少年2 天前
如果 React 组件的属性没有传值,它的默认值是什么?
前端·javascript·react.js
Fate_I_C2 天前
Capacitor 应用鸿蒙化实战:用 hionic 把 React 应用跑在 OpenHarmony 上
react.js·华为·harmonyos
liangshanbo12152 天前
面试题:React.memo 和 useMemo 的区别
前端·react.js·前端框架