React 自定义 Hook:把响应式与副作用装进 useXxx

Hooks 让函数组件有了状态和副作用,而自定义 Hook 让"状态逻辑"能像组件一样被复用。

一、从一个需求开始:把鼠标坐标显示在页面上

需求很简单:监听鼠标移动,把坐标 x: 123 y: 456 实时显示在页面上。

最直白的写法,把逻辑全部塞进组件里:

jsx 复制代码
import { useState, useEffect } from 'react'

const App = () => {
  const [x, setX] = useState(null)
  const [y, setY] = useState(null)

  useEffect(() => {
    document.addEventListener("mousemove", handleMouseMove)
    return () => {
      document.removeEventListener("mousemove", handleMouseMove)
    }
    function handleMouseMove(e) {
      setX(e.clientX)
      setY(e.clientY)
    }
  }, [])

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

能跑。但问题很明显:

  1. 逻辑和 UI 混在一起------这段"订阅事件、更新坐标"的逻辑和渲染长得一模一样,谁也说不清它属于哪个组件。
  2. 没法复用------另一个组件也想要鼠标坐标?复制粘贴一整段,改个名字,以后两处要同步改 bug。

二、抽成自定义 Hook:useMouse

把这段逻辑从组件里"提"出来,放进一个 use 开头的函数:

js 复制代码
// src/hooks/useMouse.js
import { useState, useEffect } from 'react'

const useMouse = () => {
  const [x, setX] = useState(null)
  const [y, setY] = useState(null)

  useEffect(() => {
    document.addEventListener("mousemove", handleMouseMove)
    // 组件卸载后,不会主动回收监听器,需要手动清理
    return () => {
      document.removeEventListener("mousemove", handleMouseMove)
    }
    function handleMouseMove(e) {
      setX(e.clientX)
      setY(e.clientY)
    }
  }, [])

  return { x, y }
}

export default useMouse

组件瞬间变薄:

jsx 复制代码
import useMouse from './hooks/useMouse'

const App = () => {
  const { x, y } = useMouse()
  return <div>{x && y ? `x:${x} y:${y}` : "鼠标未移动"}</div>
}

任何组件想要鼠标坐标,一行 useMouse() 就拿走。这就是自定义 Hook 的核心价值:把"响应式状态 + 副作用"打包成一个可复用的函数。

三、三个必须讲清楚的点

1. use 开头是约定,不是语法

自定义 Hook 必须以 use 开头。它不改变函数的行为,而是让 ESLint 的 react-hooks 插件和阅读代码的人能一眼认出"这是一个 Hook",从而保证两条铁律被遵守:

  • Hook 只能在函数组件其他 Hook 的顶层调用;
  • 不能在 if / for / 普通函数里调用(否则状态顺序会错乱)。

所以命名 useMouseuseTheme,不是随便起的。

2. 为什么是"Hook",而不是普通函数?

这是最关键的一点:

普通函数里不能调用 useState / useEffect / useContext。只有 Hook 或组件能调用 Hook。

所以"把响应式和副作用封装进去"这件事,只能用自定义 Hook 做 。普通函数只能做纯逻辑封装(比如格式化时间 formatDate()),一旦想塞进去一个 useState,它就必须升级成 useXxx

3. 清理副作用:谁添加,谁清理

useMouse 里的 useEffect 返回了一个函数:

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

useEffect 的返回值会在组件卸载时执行。为什么要清?

  • mousemove 是挂在 document 上的全局监听器,组件卸载后它不会自动消失;
  • 不清的话,监听器会一直挂在 document 上------内存泄漏
  • 而且监听器里的 setX 还在继续执行,去更新一个已经卸载组件的状态,React 会警告。

所以规则是:在 effect 里添加了什么,就在清理函数里移除什么。 订阅(addEventListener)、定时器(setInterval)、网络请求,都要有对应的清理。

四、更高阶:自定义 Hook 里还能消费 Context

useMouse 封装的是"浏览器事件 + state"。但自定义 Hook 的能力不止于此------它还能消费 Context ,把 useContext 一起包进去。

js 复制代码
// src/hooks/useTheme.js
import { useContext } from 'react'
import { ThemeContext } from '../ThemeContext'

export function useTheme() {
  return useContext(ThemeContext)
}

这样,PageChild 等任何深层组件,都只需要一句:

js 复制代码
const theme = useTheme()   // 我要主题

而不是:

js 复制代码
import { ThemeContext } from '../ThemeContext'
const theme = useContext(ThemeContext)   // 我得先知道有个叫 ThemeContext 的东西

这么包一层的价值,在消费方那边才看得见:

① 消费方不关心数据从哪来。 useTheme() 是语义(我要主题),useContext(ThemeContext) 是实现(去读那个 context)。将来 Provider 里塞进 isDarkfontSize,消费方依然是 useTheme(),不用改。

② 接口稳定,内部随便换。 今天是 useContext(ThemeContext),明天你把它换成多个 context 合并、甚至换成别的状态方案,只要 useTheme 的返回值不变,所有消费者零改动

③ 集中做派生逻辑。 消费方可以拿到更友好的 API:

js 复制代码
export function useTheme() {
  const theme = useContext(ThemeContext)
  return { theme, isDark: theme === "dark" }   // 消费方直接拿 isDark
}

④ 生态惯例。 Redux 的 useSelectoruseDispatch,React Router 的 useNavigate,全是这个套路------把复杂的内部机制收口成一个语义化的 useXxx

五、hooks 目录:逻辑层的架构意义

自定义 Hook 通常放在 src/hooks/ 目录下。它代表一种分层:

css 复制代码
src/
├── components/     ← UI 层:只负责渲染,越薄越好
├── hooks/          ← 逻辑层:响应式状态、副作用、context 消费,都收敛到这里
└── ThemeContext.jsx

组件只管"长什么样",Hook 管"状态怎么变、数据从哪来"。这样组件变薄、逻辑可复用、测试也能单独针对 hook 写

六、小结:什么时候该抽自定义 Hook

满足以下任一情况,就值得抽:

  1. 一段状态 / 副作用逻辑在多个组件里重复(鼠标坐标、滚动位置、定时器、防抖);
  2. 一段逻辑和 UI 无关,纯粹是"状态 + 副作用",适合独立成单元(发请求、订阅、读写本地存储);
  3. 多个组件要消费同一个 Context (主题、登录用户、语言)------抽成 useThemeuseUser

命名就一个铁律:以 use 开头

自定义 Hook 不是魔法,它只是"以 use 开头的、内部可以调用别的 Hook 的函数"。把响应式和副作用装进 useXxx,组件就只管漂亮地渲染。

相关推荐
YHHLAI7 小时前
React + TypeScript 实战:从零构建颜色选择器应用
前端·react.js·typescript
光影少年8 小时前
react navite调试方案:Flipper、远程调试
前端·javascript·react native·react.js·前端框架
YHHLAI9 小时前
[特殊字符] 从 JSX 到 TypeScript:React 开发的进化之路
javascript·react.js·typescript
zzzzzz31010 小时前
36K stars 之外:如何阅读 react-bits 这样的动画交互式 React 项目
javascript·react.js·动效
Eiceblue1 天前
React 项目实战:用 JavaScript 合并多个本地 Excel 文件
前端·javascript·react.js·excel
CIO_Alliance1 天前
AI提示系列(2)| Few-shot与ReAct有何不同? 大模型工具调用的底层逻辑详解
前端·人工智能·深度学习·神经网络·react.js·前端框架·ai+ipaas
为你学会写情书1 天前
React Hooks 完全指南:从内置 Hook 到自定义 Hook,一文彻底掌握
react.js
为你学会写情书1 天前
React Router 完全指南:从入门到实战,一文搞懂路由核心技巧
react.js
xm_xm_xm_12 天前
react17版本以前类组件常用操作
前端·javascript·react.js