绕开层层 props 搬运:我把 useContext 和自定义 Hook 跑通了

先说说我为啥要折腾这玩意儿

之前写嵌套组件的时候最闹心的一件事,就是传数据。就拿主题色来说吧,最顶层的 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 的什么奇葩坑,或者有更好用的封装思路,评论区留个言,我也学习学习。

相关推荐
北凉温华4 小时前
Vue3 可视化打印设计插件|拖拽生成票据/标签打印模板(开源体验版)
前端
程序员黑豆4 小时前
Java变量详解:从入门到精通
java·前端·ai编程
andr_gale5 小时前
02_uniapp自定义上凸效果的底部TabBar
前端·javascript·uni-app·移动端
一心只读圣贤书5 小时前
AI 驱动前端测试实战:从需求文档到 Playwright 自动化用例
前端·人工智能
拖孩5 小时前
用 AI 重解千年观音灵签,做了一个微信小程序,每天摇一摇,命运给你回应
前端·后端·微信小程序
難釋懷5 小时前
Nginx日志
前端·网络·nginx
Web极客码6 小时前
用 Python 搭建工具调用 Agent 的调试过程
java·服务器·前端
2401_881828326 小时前
简易文本处理网页工具|AI 通识课第三次作业
前端·javascript·html
Ai拆代码的曹操6 小时前
Dubbo 2.7.5 的线程上下文陷阱:从版本演进看 AllChannelHandler 的设计之痛
前端·dubbo·safari