一、为什么我们需要自定义Hook?
想象一下,你正在搭建一座乐高城堡。如果每次需要一扇窗户,都要从零开始烧制玻璃、切割木框、安装铰链......那工程得有多浩大?聪明的做法是:提前做好标准化的窗户模块,需要时直接拿来用。
自定义Hook在React中扮演的就是这个"标准化模块"的角色。
在传统React开发中,我们常常遇到这样的困境:
多个组件都需要监听鼠标悬停状态
每个组件都重复编写useState + 事件处理逻辑
代码冗余,维护成本直线上升
而自定义Hook的出现,让这一切变得优雅:
- 将通用逻辑抽离成独立的Hook函数
- 一处编写,处处复用
- 逻辑与视图分离,代码更清晰
根据2025年React开发者调查报告,87%的开发者 在项目中积极使用自定义Hook提升代码复用率,采用自定义Hook的项目平均减少30%的重复逻辑代码。
二、自定义Hook的核心规则
在深入实战之前,我们必须牢记自定义Hook的"三条铁律":
| 规则 | 说明 | 违反后果 |
|---|---|---|
| 命名规范 | 必须以 use 开头 |
ESLint报错,无法被识别为Hook |
| 调用位置 | 只能在函数组件或另一个Hook中调用 | 违反Hooks规则,导致状态错乱 |
| 纯函数原则 | 不产生意外副作用 | 难以调试和测试 |
下面就一起来看看实战案例吧🙂
三、实战案例一:useHover ------ 让交互状态"触手可及"
3.1 需求分析
假设我们需要在多个组件中实现"鼠标悬停"效果。传统做法是在每个组件中重复编写:
jsx
// 传统写法 - 每个组件都要重复
function Button() {
const [hovered, setHovered] = useState(false)
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{hovered ? '松开我' : ' hover 我'}
</div>
)
}
这就像每次做饭都要重新发明锅碗瓢盆一样低效。让我们用自定义Hook来封装这个逻辑。
3.2 核心实现
jsx
// hooks/useHover.js
import { useState, cloneElement } from "react"
export default function useHover(element) {
// 状态管理:追踪悬停状态
const [state, setState] = useState(false)
// 事件包装器:保留原有事件处理函数
const onMouseEnter = (originalOnMouseEnter) => {
return (event) => {
originalOnMouseEnter?.(event) // 先执行原有逻辑
setState(true) // 再更新悬停状态
}
}
const onMouseLeave = (originalOnMouseLeave) => {
return (event) => {
originalOnMouseLeave?.(event)
setState(false)
}
}
// 支持函数组件或元素两种传入方式
if (typeof element === 'function') {
element = element(state)
}
// 克隆元素并注入事件处理
const el = cloneElement(element, {
onMouseEnter: onMouseEnter(element.props.onMouseEnter),
onMouseLeave: onMouseLeave(element.props.onMouseLeave)
})
return [el, state] // 返回增强后的元素和状态
}
3.3 使用示例
jsx
// App3.js
import useHover from './hooks/useHover'
export default function App3() {
// 方式一:传入渲染函数(推荐)
const element = (hovered) => {
return <div>
Hover me! {hovered && 'Thanks!'}
</div>
}
const [hoverable, hovered] = useHover(element);
return (
<div>
{hoverable}
{hovered ? 'ok' : 'no'}
</div>
)
}
3.4 设计亮点解析
这个Hook的设计有三个巧妙之处:
- 事件链保护 :使用
originalOnMouseEnter?.(event)确保原有事件处理不被覆盖 - 灵活传入:支持直接传入元素或渲染函数,适应不同场景
- 双重返回:既返回增强后的元素,也返回状态值,调用方按需取用
这个Hook就像一个"智能外套",给任何元素穿上后,它就能自动感知鼠标进出,同时不改变元素原有的功能。
四、实战案例二:useMountedState ------ 感知组件的"生命脉搏"
4.1 场景痛点
在异步操作频繁的场景中,我们经常遇到这样的问题:组件已经卸载,但异步回调还在执行,试图更新已不存在的状态,导致内存泄漏或警告。
vbnet
⚠️ 典型错误:
Warning: Can't perform a React state update on an unmounted component.
4.2 实现方案
jsx
// hooks/useMountedState.js
import { useEffect, useRef, useCallback } from 'react'
export default function useMountedState() {
// 使用ref存储挂载状态(避免触发重渲染)
const isMountedRef = useRef(false)
useEffect(() => {
isMountedRef.current = true
// 清理函数:组件卸载时执行
return () => {
isMountedRef.current = false
}
}, [])
// 返回一个函数,供外部调用检查
return useCallback(() => isMountedRef.current, [])
}
4.3 使用示例
jsx
// App.js
import React, { useEffect, useRef, useState } from 'react'
import useMountedState from './hooks/useMountedState'
export default function App() {
const isMounted = useMountedState();
const [num, setNum] = useState(0)
const divRef = useRef(null)
useEffect(() => {
setTimeout(() => {
// 安全更新:先检查组件是否还在
if (isMounted()) {
setNum(1)
}
}, 1000)
}, [])
return (
<div ref={divRef}>
{ isMounted() ? '组件挂载完成' : '组件还在编译'}
</div>
)
}
4.4 关键设计点
| 设计选择 | 原因 |
|---|---|
使用 useRef 而非 useState |
避免状态变更触发不必要的重渲染 |
| 返回函数而非布尔值 | 确保每次调用都获取最新状态 |
配合 useCallback |
保持返回函数的引用稳定 |
这个Hook就像组件的"心跳监测仪",随时告诉你组件是否还"活着",避免对已"离世"的组件进行操作。
五、实战案例三:useLifecycles ------ 生命周期管理的"瑞士军刀"
5.1 从Class到Hooks的跨越
在Class组件时代,我们有清晰的生命周期方法:
componentDidMount → 组件挂载
componentWillUnmount → 组件卸载
Hooks时代,这些被 useEffect 统一接管,但有时我们需要更精细的控制。
5.2 实现代码
jsx
// hooks/useLifecycles.js
import { useEffect } from 'react'
export function useLifecycles(mount, unmount) {
useEffect(() => {
// 挂载时执行
if (mount) mount()
// 返回清理函数(卸载时执行)
return () => {
if (unmount) unmount()
}
}, []) // 空依赖数组,只执行一次
}
5.3 使用示例
jsx
// App2.js
import React, { useState } from 'react'
import { useLifecycles } from 'react-use'
const Child = () => {
useLifecycles(
null, // 挂载时不执行任何操作
() => {
console.log('Child 卸载'); // 卸载时清理资源
}
)
return <h1>child组件</h1>
}
export default function App2() {
const [show, setShow] = useState(true)
return (
<div>
<h1 onClick={() => setShow(!show)}>App2</h1>
{show && <Child></Child>}
</div>
)
}
5.4 适用场景
- 资源清理:取消订阅、清除定时器
- 埋点统计:记录组件停留时间
- 调试日志:追踪组件生命周期
这个Hook就像房子的"入住/退租登记",入住时登记信息,退租时清理房间,确保资源不泄露。
六、性能优化建议
jsx
// ✅ 推荐:使用useCallback稳定返回函数
function useCustomHook() {
const handler = useCallback(() => {
// 处理逻辑
}, [])
return handler
}
// ✅ 推荐:使用useMemo缓存计算结果
function useExpensiveCalculation(data) {
return useMemo(() => {
return heavyComputation(data)
}, [data])
}
七、结语:让Hook成为你的第二本能
学习自定义Hook的过程,就像学习一门新的语言。起初你可能觉得语法陌生,但当你开始用它的思维方式思考问题时,你会发现:
- 代码变少了:重复逻辑被抽离
- 可读性高了:意图通过Hook名称一目了然
- 维护轻松了:修改一处,全局生效
最后建议:不要为了用Hook而用Hook。只有当逻辑真正需要复用时,才值得抽离成自定义Hook。过早优化是万恶之源。