前言
在 React 开发中,Hooks 的出现彻底改变了逻辑复用的方式。它让我们能够将复杂的、可复用的逻辑从 UI 组件中抽离,实现真正的“关注点分离”。本文将分享 Hooks 的核心原则,并提供 4 个在真实业务场景中封装的实战案例。
一、 Hooks 核心
1. 概念理解
Hooks 本质上是将组件间共享的逻辑抽离并封装成的特殊函数。
2. 使用“红线”:规则与原理
-
命名规范:必须以
use 开头(如 useChat),这不仅是约定,也是静态检查工具(ESLint)识别 Hook 的依据。
-
调用位置:严禁在循环、条件判断或嵌套函数中调用 Hook。
底层原理: React 内部并不是通过“变量名”来记录 Hook 状态的,而是通过链表 。每次渲染时,React 严格依赖 Hook 的调用顺序来查找对应的状态。
注意: 如果在 if 语句中调用 Hook,一旦条件不成立导致某次渲染跳过了该 Hook,整个链表的指针就会错位,导致状态读取异常。
二、 实战:自定义 Hooks 封装
1. AI 场景:消息点赞/点踩逻辑 (useChatEvaluate)
在 AI 对话系统中,消息评价是通用功能。我们需要处理:状态切换(点赞 -> 取消点赞)、单选逻辑、以及异步接口调用。
import React, { useState } from 'react';
// 模拟接口
const public_evaluateMessage = async (params: any) => ({ data: true });
type EvaluateType = "GOOD" | "BAD" | "NONE";
export const useChatEvaluate = (initialType: EvaluateType = "NONE") => {
const [ratingType, setRatingType] = useState<EvaluateType>(initialType);
const evaluateMessage = async (contentId: number, type: "GOOD" | "BAD") => {
let newEvaluateType: EvaluateType;
// 逻辑:如果点击已选中的类型,则取消选中(NONE);否则切换到新类型
if (type === "GOOD") {
newEvaluateType = ratingType === "GOOD" ? "NONE" : "GOOD";
} else {
newEvaluateType = ratingType === "BAD" ? "NONE" : "BAD";
}
try {
const res = await public_evaluateMessage({
contentId,
ratingType: newEvaluateType,
content: "",
});
if (res.data === true) {
setRatingType(newEvaluateType);
}
} catch (error) {
console.error("评价失败:", error);
}
};
return { ratingType, evaluateMessage };
};
// 使用示例
const ChatMessage: React.FC<{ id: number }> = ({ id }) => {
const { ratingType, evaluateMessage } = useChatEvaluate();
return (
<button onClick={() => evaluateMessage(id, "GOOD")}>
{ratingType === "GOOD" ? "👍 已点赞" : "👍 点赞"}
</button>
);
};
2. 响应式布局:屏幕尺寸监听 (useMediaSize)
在响应式系统中,封装一个能根据窗口宽度自动切换“设备类型”的 Hook,可以极大地简化响应式开发。
import { useState, useEffect, useMemo } from 'react';
export enum MediaType {
mobile = 'mobile',
tablet = 'tablet',
pc = 'pc',
}
const useMediaSize = (): MediaType => {
const [width, setWidth] = useState<number>(globalThis.innerWidth);
useEffect(() => {
const handleWindowResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleWindowResize);
// 记得清理事件监听
return () => window.removeEventListener('resize', handleWindowResize);
}, []);
// 使用 useMemo 避免每次渲染都重新运行计算逻辑
const media = useMemo(() => {
if (width <= 640) return MediaType.mobile;
if (width <= 768) return MediaType.tablet;
return MediaType.pc;
}, [width]);
return media;
};
export default useMediaSize;
3. 性能优化:防抖与节流 Hook
A. 防抖 Hook (useDebounce)
常用于搜索框,防止用户快速输入时频繁触发请求。
import { useState, useEffect } from 'react';
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// 关键:在下一次 useEffect 执行前清理上一次的定时器
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;
B. 节流 Hook (useThrottle)
常用于滚动加载、窗口缩放,确保在规定时间内只执行一次。
import { useState, useEffect, useRef } from 'react';
function useThrottle<T>(value: T, delay: number): T {
const [throttledValue, setThrottledValue] = useState<T>(value);
const lastExecuted = useRef<number>(Date.now());
useEffect(() => {
const now = Date.now();
const remainingTime = delay - (now - lastExecuted.current);
if (remainingTime <= 0) {
// 立即执行
setThrottledValue(value);
lastExecuted.current = now;
} else {
// 设置定时器处理剩余时间
const timer = setTimeout(() => {
setThrottledValue(value);
lastExecuted.current = Date.now();
}, remainingTime);
return () => clearTimeout(timer);
}
}, [value, delay]);
return throttledValue;
}
export default useThrottle;
三、 总结:封装自定义 Hook 的心法
-
抽离状态而非仅逻辑:如果一段逻辑只涉及纯函数计算,不需要 Hook;只有涉及
useState 或 useEffect 等状态管理时,才有必要封装 Hook。
-
保持纯净:自定义 Hook 应该只关心逻辑,而不应该直接操作 DOM。
-
TS 类型保护:利用泛型
<T> 增强 Hook 的兼容性,让它能适配各种数据类型。