不要把 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 小时前
赋予 AI “灵魂”:LangChain.js 中临时与长期记忆的终极实战指南
前端·javascript
想要成为糕糕手1 小时前
🧵 浏览器里的第二大脑:Web Worker
javascript·react.js·浏览器
逍遥德2 小时前
ECMAScript 各个版本的语法列表
前端·javascript·ecmascript·es6
weixin_BYSJ19872 小时前
【java项目分享】springboot阅读推荐平台10600
java·javascript·spring boot·python·django·flask·php
Sterting2 小时前
第 9 节:本地存储 — 数据不丢的前端缓存
前端·javascript·缓存
breeze jiang2 小时前
React useRef + Web Worker:避免大计算阻塞页面的通信方案
前端·javascript·react.js
油丶酸萝卜别吃3 小时前
encrypt.js — 前端密码哈希工具说明文档
前端·javascript·哈希算法
黄贵根9 小时前
JavaScript实现教培行业意向登记系统
开发语言·javascript·ecmascript
zzzzzz31013 小时前
从 react-bits 看动效组件化:别把视觉效果写成一次性页面代码
javascript·react.js·开源