React Context + 自定义 Hooks:告别 Props 地狱,一行代码跨层级传数据

0 阅读6分钟

React Context + 自定义 Hooks:告别 Props 地狱,一行代码跨层级传数据

从"层层传递 props"的噩梦讲起,用主题切换和鼠标追踪两个实例,带你彻底搞懂 React Context 和自定义 Hooks。

前言

你有没有遇到过这种情况:一个数据在最外层组件获取,但真正用到它的却是第五层嵌套的子组件?你不得不在中间的每一层都加上 props 传递,哪怕那些中间组件根本不关心这个数据。

React Context 就是来解决这个问题的。这篇文章适合刚接触 React、已经会写 useState 和父子组件通信,但还没搞懂"跨层级传数据"怎么优雅实现的你。

读完你将收获:理解 Context 三件套(createContext / Provider / useContext)、学会封装自定义 Hooks、看懂一个完整项目的架构思路。

文末附有完整可运行项目代码,可以直接 clone 下来跑起来看效果。

项目概览

这个项目用两个 Demo 展示了 Context 和自定义 Hooks 的用法:

Demo文件演示内容
鼠标追踪App.jsx + useMouse.js自定义 Hook 封装鼠标坐标
主题切换App2.jsx + Theme.Context.jsx + useTheme.jsContext 跨层级共享主题

组件树

App
├── ThemeContext.Provider(提供主题数据)
│   └── Page(消费主题,显示当前 theme 值)
│       └── Child(消费主题,按钮随主题变色)
└── button(点击切换 light / dark)

用到的核心技术

  • createContext — 创建上下文容器
  • .Provider — 提供数据
  • useContext — 消费数据
  • 自定义 Hooks(useThemeuseMouse
  • useState + useEffect

核心知识点一:Context — 跨层级的数据管道

问题:Props 地狱长什么样?

假设没有 Context,你要把一个 theme 数据从 App 传给最深层的 Child

// ❌ props 层层搬运,中间组件被迫传递自己不用的数据
function App() {
    const theme = 'dark';
    return <Page theme={theme} />;
}

function Page({ theme }) {
    return <Child theme={theme} />;  // Page 自己根本不用 theme
}

function Child({ theme }) {
    return <button className={theme}>按钮</button>;  // 只有它真正需要
}

项目 App2.jsx 里有一段被注释掉的代码,恰好就是这种"逐层传递"的写照:

// App2.jsx 注释代码
// <Parent>
//   <Child>
//     <GrandChild>
//       <GreatGrandChild></GreatGrandChild>
//     </GrandChild>
//   </Child>
// </Parent>

如果嵌套到第四层、第五层,每层都要做 props "搬运工",代码又乱又容易出错。

解决:Context 三件套

React Context 就像一条直达管道——数据在最外层注入,任意深层组件都能直接读取,中间层无需参与。

第一步:createContext 创建管道
// Theme.Context.jsx
import { createContext } from 'react';

export const ThemeContext = createContext("light");

createContext("light") 做了两件事:

  1. 创建一个上下文对象(管道)
  2. "light" 作为默认值——当组件树没有被 Provider 包裹时,取这个值
第二步:Provider 注入数据
// App2.jsx
function App() {
    const [theme, setTheme] = useState('light');

    return (
        <ThemeContext.Provider value={theme}>
            <Page />
            <button onClick={() =>
                setTheme(theme === 'light' ? 'dark' : 'light')
            }>切换主题</button>
        </ThemeContext.Provider>
    );
}

Provider 是一个"数据发射器",value={theme} 告诉它当前要共享的数据是什么。

重点:Provider 不需要放在全局——你想让哪个子树共享数据,就在哪里包裹。这里的 ThemeContext.Provider 只包裹了 <Page /> 和按钮,这就是它的"作用范围"。

第三步:useContext 消费数据

项目中 Page 和 Child 都直接读取了 theme,但没有经过任何中间层传递

// Page.jsx
const Page = () => {
    const theme = useTheme();   // 直接用,不需要从 App 传下来
    return (
        <>
            Page {theme}
            <br />
            <Child />             {/* 传给 Child 时根本没写 theme prop */}
        </>
    );
};

// Child.jsx
function Child() {
    const theme = useTheme();   // 直接取!
    return <button className={theme}>按钮{theme}</button>;
}

Context 的数据流转图:

ThemeContext.Provider value="dark"
        │
        ├── Page ──useContext()──→ "dark"  ✅ 跳过 App,直接读
        │     │
        │     └── Child ──useContext()──→ "dark"  ✅ 跳过 Page,直接读

每一层都跳过了 props,直接"伸手"从 Context 里取。这就是跨层级的本质。

核心知识点二:自定义 Hooks — 把"能力"封装成一行调用

是什么?

自定义 Hook 就是一个以 use 开头的函数,它可以调用 React 内置 Hooks(useState、useEffect 等),然后把响应式逻辑打包成一个可复用的"工具箱"。

项目中封装了两个自定义 Hook:

useTheme:让 Context 消费更优雅

// hooks/useTheme.js
import { ThemeContext } from '../Theme.Context.jsx';
import { useContext } from 'react';

export function useTheme() {
    return useContext(ThemeContext);
}

为什么多此一举封装一层? 直接在每个组件里写 useContext(ThemeContext) 不就完了?

三个原因:

  1. 语义化useTheme()useContext(ThemeContext) 读起来更直观
  2. 解耦:假如将来 Context 从 ThemeContext 换成 ReduxZustand,只需要改 useTheme.js 一个文件,所有组件无感知
  3. IDE 友好:输入 useTheme() 自动提示清晰,不会跟其他 Context 搞混

useMouse:封装副作用逻辑

这是项目另一个 Demo —— 追踪鼠标坐标:

// hooks/useMouse.js
import { useState, useEffect } from 'react';

export const useMouse = () => {
    const [x, setX] = useState(0);
    const [y, setY] = useState(0);

    useEffect(() => {
        const handleMouseMove = (e) => {
            setX(e.clientX);
            setY(e.clientY);
        };
        document.addEventListener('mousemove', handleMouseMove);

        return () => {
            document.removeEventListener('mousemove', handleMouseMove);
        };
    }, []);

    return { x, y };
};

逐行拆解:

代码作用
const [x, setX] = useState(0)响应式存储鼠标 X 坐标,初始值 0
const [y, setY] = useState(0)Y 坐标同上
useEffect(..., [])组件挂载时执行一次,[] 表示不依赖任何变量
document.addEventListener('mousemove', handleMouseMove)监听全局鼠标移动事件
handleMouseMove每次鼠标移动,更新 x、y 状态
return () => removeEventListener(...)清理函数——组件卸载时移除监听,防止内存泄漏
return { x, y }对外暴露坐标,让调用方想怎么用就怎么用

使用方极其简洁

// App.jsx
function App() {
    const { x, y } = useMouse();

    return (
        <div>
            {x && y ? `x:${x}, y:${y}` : '鼠标未移动'}
        </div>
    );
}

一行 useMouse() 就搞定了所有状态管理 + 事件监听 + 清理逻辑。这就是自定义 Hook 的威力——把复杂的响应式逻辑封装成一个"黑盒",使用时只需要一行调用。

知识串联:Context + 自定义 Hooks = 完美搭档

把两者结合起来,就形成了项目中这套架构模式:

Theme.Context.jsx          ←  createContext 建管道
        ↓
hooks/useTheme.js          ←  自定义 Hook 封装 useContext
        ↓
components/Page.jsx        ←  调用 useTheme(),一行消费
components/Child.jsx       ←  同上,完全不需要 props

这就是 React 推荐的"逻辑层 → 视图层"分层方式

  • Context 负责"数据管道"
  • 自定义 Hooks 负责"消费数据的逻辑"
  • 组件只负责"渲染 UI"

总结

回顾你从这个项目能掌握的:

  1. Context 是跨层级共享数据的管道——createContext 建管道,Provider 注数据,useContext 取数据
  2. 不需要把 Provider 包在全局——包在哪个子树,数据就在哪个范围共享
  3. 自定义 Hook 本质是逻辑封装——把 useState、useEffect 等打包成一行调用的"黑盒"
  4. useTheme 展示了"解耦"的价值——组件不需要知道 Context 的具体实现
  5. useMouse 展示了"副作用管理"的规范写法——监听、更新、清理三步走,useEffect 里不要忘记 return 清理函数

下一步可以尝试的扩展

  • useMouse 加一个节流(throttle),减少 setState 频率
  • 用 Context 实现全局的用户登录状态(UserContext,提供 usersetUser
  • 学习 useReducer + Context 的组合,处理更复杂的状态逻辑

完整项目代码

项目托管在 Gitee,可以直接 clone 运行:

# 1. 克隆仓库
git clone git@gitee.com:dcx2758/ai_doubao_dcx.git
cd ai_doubao_dcx/dcx/fe/react/basic/context-demo/context-demo/

# 2. 安装依赖
npm install

# 3. 启动开发服务器
npm run dev

项目文件树

context-demo/
├── index.html
├── package.json
├── vite.config.js
├── eslint.config.js
├── src/
│   ├── main.jsx               # 入口文件
│   ├── App.jsx                # Demo1: useMouse 鼠标追踪
│   ├── App2.jsx               # Demo2: Context 主题切换(手动改为 main.jsx 引入)
│   ├── Theme.Context.jsx      # createContext 创建主题上下文
│   ├── index.css              # 全局样式(CSS 变量 + 暗色模式)
│   ├── App.css                # App 样式
│   ├── hooks/
│   │   ├── useMouse.js        # 自定义 Hook: 鼠标坐标
│   │   └── useTheme.js        # 自定义 Hook: 消费主题上下文
│   └── components/
│       ├── Page.jsx           # 页面组件,消费主题
│       └── Child.jsx          # 子组件,消费主题

依赖及版本

依赖版本
react^19.2.6
react-dom^19.2.6
vite^8.0.12
@vitejs/plugin-react^6.0.1

提示:默认 main.jsx 引入的是 App.jsx(鼠标追踪 Demo)。如果想体验主题切换 Demo,把 import App from './App.jsx' 改为 import App from './App2.jsx',然后在切换主题的页面中观察 Page 和 Child 如何跨层级拿到数据。


你觉得 Context 这种方式怎么样?在你的项目里有没有遇到过"props 地狱"?欢迎在评论区交流 👏