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

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.js Context 跨层级共享主题

组件树

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

用到的核心技术

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

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

问题:Props 地狱长什么样?

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

jsx 复制代码
// ❌ 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 里有一段被注释掉的代码,恰好就是这种"逐层传递"的写照:

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

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

解决:Context 三件套

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

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

export const ThemeContext = createContext("light");

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

  1. 创建一个上下文对象(管道)
  2. "light" 作为默认值------当组件树没有被 Provider 包裹时,取这个值
第二步:Provider 注入数据
jsx 复制代码
// 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,但没有经过任何中间层传递

jsx 复制代码
// 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 的数据流转图:

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

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

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

是什么?

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

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

useTheme:让 Context 消费更优雅

jsx 复制代码
// 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 ------ 追踪鼠标坐标:

jsx 复制代码
// 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 } 对外暴露坐标,让调用方想怎么用就怎么用

使用方极其简洁

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

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

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

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

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

scss 复制代码
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 运行:

bash 复制代码
# 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

项目文件树

bash 复制代码
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 地狱"?欢迎在评论区交流 👏

相关推荐
moMo1 小时前
前端路由,其实就是三个对象在演戏
前端·react.js
用户2930750976691 小时前
React 自定义 Hooks 实战:用 useMouse 理解响应式封装
前端
触底反弹1 小时前
🔥 别再只用 useRef 拿 DOM 了!这 5 个实战场景让你彻底理解它(附源码解析 + 面试题)
前端·javascript·react.js
玉宇夕落1 小时前
useRef + Web Worker —— 从零到一理解React多线程协作原理
前端
用户6919026813391 小时前
React useRef、useEffect、useState 与 Web Worker 多线程实践
前端
BreezeJiang1 小时前
Web Worker 不负责渲染:React 中的线程分工与消息闭环
javascript·react.js
烬羽1 小时前
useContext 用是用了,但你真的用对了吗?——把 Context 封装进自定义 Hook
前端·react.js·全栈
做前端的娜娜子1 小时前
JavaScript 闭包
前端·javascript·掘金·金石计划
xiaominlaopodaren1 小时前
three.js地图数学基础(一)
前端·three.js