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>
}

PageApp 之间隔着多少层都无所谓,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、给 valueuseMemo,或上更专业的状态方案。

一句话总结:

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

相关推荐
不好听6131 小时前
React 自定义 Hook:把响应式与副作用装进 useXxx
react.js
触底反弹1 小时前
🏗️ 写完 Todos 之后,大型 React 项目的 7 个架构真相
前端·react.js·前端框架
kisshyshy1 小时前
从多页面到SPA:React Router 路由进阶完全指南
前端·javascript·react.js
Zzj_tju4 小时前
Tool-Using LLM 论文精读路线:从 ReAct 到可验证工具调用
前端·react.js·前端框架
To_OC11 小时前
别再瞎写 React Router!7 个高频踩坑点一次性讲透
前端·javascript·react.js
kyriewen15 小时前
我踩了三次同一个坑才明白:React里"改了数据却不重新渲染"的真正原因
前端·javascript·react.js
A242073493016 小时前
React中请求拦截与响应拦截的统一处理方案
前端·javascript·react.js
烬羽17 小时前
《受控 vs 非受控:你以为用对了 useState,直到你写了那个表单》
javascript·react.js·前端框架
嘟嘟071717 小时前
React 受控组件与非受控组件:表单数据到底归谁管?
前端·javascript·react.js