当组件树的深度让 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>
)
}
两个关键点:
value绑定的不是常量,而是 state 。所以 Provider 提供的值是响应式 的------setTheme一执行,所有消费方自动重新渲染。这是 context 和普通全局变量最大的区别。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 都是局部的一份,就近原则决定谁喝到哪一份。