不要把 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)
}

Page 和 Child 调用 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、监听注册、卸载清理"从页面组件移走。clientX、clientY 是鼠标相对浏览器可视区域的坐标。

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

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

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() 获得坐标,但仍导入了未使用的 useState 与 useEffect。本次未执行 lint,因此不把它写成已确认错误。

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

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

相关推荐
程序员大阳2 小时前
HTML+CSS+JS对话式实战(3)-活动管理页实现查询、新增、删除API后端接口
javascript·css·html·实战·删除·查询·新增
炫酷的大高个3 小时前
6 亿播放的闪身步,我用前端算出 15 倍重心差
前端·javascript
林小帅4 小时前
【笔记】Prisma v7 + SQLite TS2305 及连锁 TS7006 问题
前端·javascript·后端
Dumbness4 小时前
订阅消息弹窗必须在用户点击里调:联调实验室里 timing 和 Mock 支付通知踩的坑
javascript·uni-app
黄连木笛4 小时前
不改一行 Postman 源码,把它汉化了:Electron preload 注入 + fetch 拦截实战
javascript·electron
叼烟扛炮5 小时前
[LangChain] 学习笔记(03):打破失忆症——对话机制与上下文管理
java·前端·javascript·笔记·学习·langchain
卋罖7506 小时前
Razor 视图 + JavaScript + Vue/iView 组件实现的表格自定义列
javascript·vue·iview·razor视图
系统集成-鹰翔21 小时前
ERP图片改名后仍命中识别缓存:缓存键、十分钟到期与调用计数
javascript·经验分享·后端
FlyWIHTSKY1 天前
vue2和vue3有什么区别,用法上有哪些区别
前端·javascript·vue.js
程序员大阳1 天前
副队长JavaScript教程(66)-本地存储实战:待办事项网页的开发
javascript·json·实战·待办事项·本地存储·案例