不要把 Context 当万能状态库:主题共享和鼠标 Hook 应该这样拆

不要把 Context 当万能状态库:主题共享和鼠标 Hook 应该这样拆

摘要:一个 React 学习 Demo 同时包含主题 Context 与 useMouse。它们解决的不是同一类问题:Context 用于跨层传递共享数据,自定义 Hook 用于封装可复用的状态和副作用。本文从源码调用链出发,给出 Provider、useContext、事件清理和职责划分的自检方法。代码运行未验证。

深层组件树需要主题时,很多人会把 theme 从 App 一路传到按钮。多个页面都需要鼠标位置时,又会复制 useState + useEffect + addEventListener。前者是跨层共享问题,后者是逻辑复用问题。关键不是"都用 Hook",而是选对职责:Context 管共享数据,自定义 Hook 管可复用逻辑。

从主题消费链路看 Context

Demo 先创建主题通道:

jsx 复制代码
import { createContext } from 'react'

export const ThemeContext = createContext('light')

这里的 light 不是当前主题的强制值,而是兜底值。消费者没有被对应 Provider 包裹时,useContext(ThemeContext) 才会得到它。

Provider 负责限定共享数据生效的组件树:

jsx 复制代码
<ThemeContext.Provider value="dark">
  <Page />
</ThemeContext.Provider>

它包裹的 Page 和后代可以读取 dark。如果嵌套了同一种 Provider,消费者读取最近的 Provider 值,因此内部 Provider 可以覆盖外部 Provider。

Demo 没有让组件到处直接写 useContext,而是封装为 useTheme

jsx 复制代码
import { useContext } from 'react'
import { ThemeContext } from '../ThemeContext'

export const useTheme = () => {
  return useContext(ThemeContext)
}

PageChild 调用 useTheme(),其中 Child 将读取到的主题写入按钮的 className 和文本。这说明业务组件只关心"拿主题",不必关心 Context 定义在哪个文件。

useMouse 复用的是逻辑,不是同一份坐标

鼠标 Hook 的实现包含 state 和副作用:

jsx 复制代码
export const useMouse = () => {
  const [x, setX] = useState(0)
  const [y, setY] = useState(0)

  useEffect(() => {
    function handleMouseMove(e) {
      setX(e.clientX)
      setY(e.clientY)
    }

    document.addEventListener('mousemove', handleMouseMove)

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

  return { x, y }
}

调用端只需要:

jsx 复制代码
const { x, y } = useMouse()

这段封装把"坐标 state、监听注册、卸载清理"从页面组件移走。clientXclientY 是鼠标相对浏览器可视区域的坐标。

但要注意:两个组件分别调用 useMouse() 时,它们拥有各自的 state 和 Effect。它们只是复用同一段代码,并不会天然共享一份 xy

事件监听的关键不是添加,而是成对清理

Effect 中的清理函数:

jsx 复制代码
return () => {
  document.removeEventListener('mousemove', handleMouseMove)
}

保证组件卸载后停止监听。更重要的是,添加和移除时传入的必须是同一个函数:

jsx 复制代码
document.addEventListener('mousemove', handleMouseMove)
document.removeEventListener('mousemove', handleMouseMove)

如果每次都传一个新的匿名函数,浏览器无法定位已经注册的监听器。

选择工具时只问一个问题

你的问题 选择
多个深层后代需要同一份主题、用户、语言数据 Context
多个组件需要复用请求、监听、定时器等逻辑 自定义 Hook
只有直接父子组件使用的数据 Props

Context 不是全局变量,也不应替代所有局部 state。自定义 Hook 也不是共享状态容器,它主要用来抽离重复的响应式和副作用逻辑。

一次可执行的自检

  • Provider 是否包裹了需要读取数据的组件树?
  • Consumer 读取的数据结构是否与 Provider 的 value 一致?
  • Hook 是否以 use 开头,并在组件或 Hook 顶层调用?
  • useEffect 是否释放了事件监听、定时器、Worker 或连接?
  • 添加和移除事件时是否复用了同一个函数引用?
  • 复用需求是共享数据,还是共享逻辑?

本次源码的未验证项

静态阅读发现 App.jsx 已通过 useMouse() 获得坐标,但仍导入了未使用的 useStateuseEffect。本次未执行 lint,因此不把它写成已确认错误。

已读的 App.jsx 也没有展示主题 Provider 的实际挂载位置,所以主题链路的运行状态未知。本文基于本地源码整理,代码运行未验证。

最终判断很简单:数据需要跨层抵达多个消费者时用 Context;逻辑需要被多个组件复用时写自定义 Hook。先按这个边界拆分,再决定是否需要更复杂的状态管理方案。

相关推荐
三十而立洋1 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁1 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄1 天前
JavaScript 隐式全局变量解析
javascript
沙蒿同学1 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端
paopaokaka_luck1 天前
智慧社区综合服务小程序(人脸识别、AI问答、Echarts图形化分析)
前端·javascript·spring boot·spring·数据分析·echarts
Hilaku1 天前
为什么死磕 1px 的团队,用户体验反而更差?
前端·javascript·程序员
陈拉斯1 天前
给公司年会写了个大屏抽奖系统:动画在前端跑,凭什么说结果没被改?
javascript
yzy852 天前
Vue中下载或打开文件的JavaScript方法
前端·javascript·vue
用户298698530142 天前
在 React 中用 JavaScript 将 Word 转换为文本
前端·javascript·react.js
李少兄2 天前
深入解析 JavaScript 中的 `document` 对象
javascript