先说说我为啥要折腾这玩意儿
之前写嵌套组件的时候最闹心的一件事,就是传数据。就拿主题色来说吧,最顶层的 App 里定义了 theme,Page 组件要用,Page 里面的 Child 组件也要用。按照以前的写法,就得 App 传给 Page,Page 再顺手传给 Child。这才两层,看着还行,可要是以后再加个 GrandChild、GreatGrandChild 呢?中间每一层都得干 "接过来、传下去" 的搬运活,改个变量名得改三四个文件,纯纯的体力劳动。
我就不信 React 没给这种场景留解决方案。翻了下文档,果然,useContext 就是干这个的。
Context 到底是个啥东西
我自己理解下来,Context 就像在组件树里放了一个公共储物柜。
- 你在某一层放一个
Provider,相当于把数据存进柜子里 - 这一层下面所有的子孙组件,不管隔了多少代,都能直接伸手从柜子里拿数据
- 中间那些组件完全不用知情,不用再写 props 接力
说白了就是 "跨层级直通",不用再层层中转。
我是怎么一步步搭起来的
第一步:先建一个上下文
首先得用 createContext 创建一个上下文对象,相当于先定制好这个储物柜的 "型号"。
javascript
// ThemeContext.jsx
import { createContext } from 'react';
// 创建一个主题上下文,默认值是 light
export const ThemeContext = createContext('light');
说实话,一开始我以为这个默认值是 "兜底用" 的 ------ 比如 Provider 没传 value 的时候就用它。后来才发现我理解错了,这个后面踩坑的时候细说。
第二步:用 Provider 把数据 "放" 进去
有了上下文之后,得用它的 Provider 组件把需要共享数据的子树包起来,通过 value 属性把数据传进去。
jsx
// App.jsx
import { useState } from 'react';
import { ThemeContext } from './ThemeContext';
import Page from './components/Page';
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Page />
<button onClick={() => setTheme('dark')}>切换主题</button>
</ThemeContext.Provider>
)
}
export default App;
我一开始还以为 Provider 只能包在整个应用的最根节点,后来才发现根本不是。你想在哪层提供数据,就包在哪层,作用范围就只有它下面的子组件。这点还挺灵活的,不是非得当全局变量用。
跑起来之后我打开 React DevTools 看了眼组件树,确实能看到 Context.Provider 这一层,右边清清楚楚显示着 value: "dark",下面的 Page、Child 都没接 props,数据直接透下去了。

第三步:在子孙组件里拿数据
最开始我是直接在组件里用 useContext(ThemeContext) 拿数据,写了两个组件发现重复代码有点多。反正都是读同一个上下文,不如抽成一个自定义 Hook。
javascript
// hooks/useTheme.js
import { ThemeContext } from '../ThemeContext';
import { useContext } from 'react';
// 约定俗成:自定义 Hook 必须 use 开头
export function useTheme() {
return useContext(ThemeContext);
}
封装完之后,不管在哪一层的组件,只要在 Provider 作用域内,直接调用 useTheme() 就能拿到主题值。
jsx
// components/Page.jsx
import Child from './Child';
import { useTheme } from '../hooks/useTheme';
const Page = () => {
const theme = useTheme();
return (
<>
Page {theme}
<br />
<Child/>
</>
)
}
export default Page;
jsx
// components/Child.jsx
import { useTheme } from '../hooks/useTheme';
function Child() {
const theme = useTheme();
return (
<>
Child
<button className={theme}>按钮{theme}</button>
</>
)
}
export default Child;
你看,Page 和 Child 之间完全没传 props,但都拿到了同一个 theme 数据。这要是再加个孙组件,照样直接调用 useTheme(),舒服多了。
我的项目目录长这样
我习惯把不同职责的文件分开,上下文单独一个文件,自定义 Hook 全放 hooks 目录,组件放 components 目录,找起来不费劲。
又试了个更直观的例子:监听鼠标位置
尝到自定义 Hook 的甜头之后,我又写了个 useMouse 来练手 ------ 实时获取鼠标坐标。这种逻辑如果写在组件里,下次别的页面也要用就得复制粘贴。抽成 Hook 之后,一行 useMouse() 就搞定。
javascript
// hooks/useMouse.js
import { useState, useEffect } from 'react';
export const useMouse = () => {
const [x, setX] = useState(null);
const [y, setY] = useState(null);
useEffect(() => {
function handleMouseMove(e) {
setX(e.clientX);
setY(e.clientY);
}
document.addEventListener('mousemove', handleMouseMove);
// 注意这行!坑了我半小时
return () => {
document.removeEventListener('mousemove', handleMouseMove);
}
}, [])
return {x, y}
}
说个丢人的事,第一版我没写 return 里的移除事件。后来切页面的时候控制台飘了个警告,说内存泄漏。我才反应过来:事件监听、定时器这种东西,React 不会帮你自动清理,必须在 useEffect 的清理函数里手动拆掉,不然组件卸载了事件还挂在 document 上,迟早出问题。
用起来就很简单了:
jsx
// App2.jsx
import { useMouse } from "./hooks/useMouse";
function App() {
const {x, y} = useMouse();
return (
<div style={{height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
{x && y ? `x: ${x}, y: ${y}` : '鼠标未移动'}
</div>
)
}
export default App;
跑起来的效果就是这样,鼠标一动坐标实时更新,逻辑全封装在 Hook 里,组件只管渲染。

踩过的几个坑,别再踩了
1. createContext 的默认值,不是你想的那样
我一开始以为默认值是 "Provider 没传 value 时的兜底",这么说也不算错,但更准确的说法是:只有当组件外层完全找不到对应 Provider 时,才会用上这个默认值 。只要外面包了 Provider,哪怕你 value 传了个 undefined,也不会走默认值。我之前对着默认值调了十分钟,纳闷为啥不生效,后来才搞懂这个规则。
小提示默认值更多是给脱离 Provider 单独测试组件用的,正常业务里基本都是靠 Provider 传值。
2. 自定义 Hook 必须以 use 开头
别不信邪,我一开始随手写了个 getTheme,结果 eslint 直接飘红,React 也不会把它当成 Hook 来做语法检查。这既是约定,也是规则。起名老老实实 useXxx,省得给自己找麻烦。
3. Provider 的 value 别随便传对象
这次 demo 我传的是字符串 'dark',没啥问题。但如果你要传一个对象,比如 value={{ theme, setTheme }},就要小心了。父组件每次渲染都会生成一个新的对象引用,会导致所有消费这个 Context 的组件全部跟着重渲染,层级深了性能掉得很明显。真要传对象,记得用 useMemo 包一下。
最后扯两句
跑通这两个例子之后,我对这俩东西的理解才算落地了:
- Context 解决的是跨层级数据共享的问题,适合主题、用户信息、语言包这种 "全局通用、不频繁变动" 的数据。不用再写 props 接力代码,清爽很多。
- 自定义 Hook 是真的香,它能把响应式状态 + 副作用逻辑打包成一个可复用的函数。比以前高阶组件、render props 那套写法直观太多,是 React 里复用逻辑的首选。
当然也不是万能的。Context 不适合放频繁更新的数据,不然容易引发大面积不必要的重渲染。真到了复杂状态管理的场景,该上状态管理库还是得上。
如果你也踩过 Context 或者自定义 Hook 的什么奇葩坑,或者有更好用的封装思路,评论区留个言,我也学习学习。