
一句话:自定义 Hook 是「把有状态的逻辑抽出来复用」的方式——它复用的是逻辑而不是状态(每个使用它的组件都有独立的状态);命名必须以
use开头。
一、什么是自定义 Hook
// 一个最简单的自定义 Hook
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((v) => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
// 使用(注意:两个组件里的 value 是【各自独立】的)
function ComponentA() {
const { value, toggle } = useToggle();
return ;
}
function ComponentB() {
const { value, toggle } = useToggle(true); // 各自独立的状态
return ;
}
核心特点:
| 特点 | 说明 |
|---|---|
| 复用逻辑,不复用状态 | 每个调用方有独立的 state(这是与「全局 store」的本质区别) |
| 就是普通函数 | 没有特殊语法,只是「函数内部调用了其它 Hook」 |
命名必须以 use 开头 |
否则 ESLint 无法检查 Hook 规则,且语义不清 |
| 遵循 Hook 规则 | 必须在顶层调用,内部也不能有条件/循环里的 Hook |
二、与普通函数、组件的区别
三种东西的边界:
① 纯函数(utils)
✅ 无状态、无副作用、无 Hook
例:formatDate、debounce、groupBy
② 自定义 Hook(hooks)
✅ 内部使用了 React 的 Hook(useState / useEffect / useRef…)
✅ 用于「复用有状态的逻辑」
例:useDebounce、useFetch、useLocalStorage
③ 组件(components)
✅ 返回 JSX(React 元素)
✅ 用于「复用 UI」
例:、
判断方法:
它返回 JSX 吗?→ 是 → 组件
它内部用了 Hook 吗?→ 是 → 自定义 Hook
都不是 → 普通工具函数
// 同一个需求:防抖。三种实现方式的区别
// ❌ 纯函数:无法在组件里正确使用(每次渲染都会重建定时器)
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// 在组件里用:const debounced = debounce(fn, 300); → 每次渲染都是新的闭包
// ✅ 自定义 Hook:把「跨渲染的定时器」用 useRef 保存
function useDebouncedCallback void>(fn: T, delay: number) {
const timerRef = useRef();
const fnRef = useRef(fn);
useEffect(() => { fnRef.current = fn; }); // 保存最新回调
return useCallback((...args: Parameters) => {
clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => fnRef.current(...args), delay);
}, [delay]);
// 清理(组件卸载时)
useEffect(() => () => clearTimeout(timerRef.current), []);
}
// ✅ 另一种形式:返回「防抖后的值」而不是「防抖后的函数」
function useDebouncedValue(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id); // ⭐ value 变化时清掉上一个定时器
}, [value, delay]);
return debounced;
}
三、两条命名与调用规则
① 命名必须 use 开头
✅ useDebounce / useFetch / useLocalStorage
❌ debounceValue / fetchData / localStorageHook
原因:
· ESLint 的 rules-of-hooks 靠前缀识别「可能包含 Hook 的函数」
· 语义约定(告诉使用者「这是 Hook,要遵守规则」)
② 必须在顶层调用(与 Hook 规则一致)
❌ 在条件、循环、嵌套函数里调用自定义 Hook
❌ 在事件处理里调用
// ❌ 有条件地调用自定义 Hook
function Comp({ shouldTrack }) {
if (shouldTrack) {
const position = useMousePosition(); // ❌ 违反规则
}
}
// ✅ 把条件放到 Hook 内部
function useMousePosition(enabled: boolean) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
if (!enabled) return; // ✅ 条件在内部
const onMove = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', onMove);
return () => window.removeEventListener('mousemove', onMove);
}, [enabled]);
return pos;
}
function Comp2({ shouldTrack }) {
const position = useMousePosition(shouldTrack); // ✅ 无条件调用
}
四、设计原则
① 单一职责:一个 Hook 只做一件事
❌ useUserDataAndThemeAndCart()
✅ useUser() / useTheme() / useCart()
② 返回「稳定」的东西
· 返回的函数用 useCallback(避免调用方依赖它时反复触发)
· 返回的对象/数组用 useMemo(如果调用方会把它放进依赖)
③ 返回「ref 而不是值」时要注意语义
· 返回 ref → 调用方能传给 JSX(如 useLocalStorage、useElementSize)
· 返回 [value, setValue] → 更像 useState 的用法(熟悉)
④ 接收「ref 或值」都支持(更灵活)
const el = useElementSize(containerRef); // 传 ref
const el2 = useElementSize(selector); // 也支持选择器
⑤ 清理必须内部完成(调用方不需要关心)
· 定时器、监听、Observer、订阅都在 Hook 内部清理
· 这是自定义 Hook 最大的价值之一
⑥ 不要把「纯计算」抽成 Hook
· 没有 Hook 调用的逻辑 → 普通工具函数(更易测试、更快)
// ⑤ 的价值示例:调用方完全不用操心清理
function useWindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => {
const onResize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize); // ⭐ 内部清理
}, []);
return size;
}
// 调用方极其简洁,也不会忘记清理
function Layout() {
const { width } = useWindowSize();
return {width > 768 ? '桌面' : '移动'};
}
五、返回值的两种风格
// ① 返回数组(像 useState,可以用任意名字解构)
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((v) => !v), []);
return [value, toggle] as const; // ⭐ as const 保证元组类型
}
const [isOpen, toggleOpen] = useToggle(); // 调用方可以自由命名 ✅
// ② 返回对象(调用方按需取,加字段不破坏调用方)
function useFetchData(url: string) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const refetch = useCallback(() => { /* ... */ }, [url]);
return { data, loading, error, refetch }; // ✅ 后续加字段不影响调用方
}
const { data, loading } = useFetchData('/api/users'); // 按需解构
| 风格 | 优点 | 适用 |
|---|---|---|
| 数组 | 调用方可自由命名;像 useState |
返回值少且语义固定(如 [value, setValue]) |
| 对象 | 可选择性解构;新增字段不破坏调用方 | 返回值多({ data, loading, error }) |
注意:返回对象时每次渲染都是新对象,如果调用方把它放进依赖数组会出问题。所以「需要放进依赖」的场景要么返回数组(元素自身稳定),要么在 Hook 内部用
useMemo稳定返回对象(但这通常得不偿失)。
六、高频自定义 Hook 清单
// ===== 状态类 =====
useToggle() // 布尔切换
useCounter(initial) // 计数器
useLocalStorage(key, initial) // 同步到 localStorage
useSessionStorage(key, initial)
useMap / useSet // 提供操作方法的 Map / Set
// ===== 副作用类 =====
useDebounce(value, delay) // 防抖后的值
useDebounceFn(fn, delay) // 防抖后的函数
useThrottle(value, interval)
useInterval(fn, delay) // 可动态调整间隔的 setInterval
useTimeout(fn, delay)
// ===== 浏览器 API =====
useWindowSize()
useMediaQuery(query)
useOnlineStatus()
useClickOutside(ref, handler)
useEventListener(target, type, handler)
useScrollPosition()
useIntersectionObserver(ref, options)
useResizeObserver(ref, callback)
// ===== 数据类 =====
useFetch(url, options) // 简化版请求
useAsync(fn, deps) // 通用异步
usePrevious(value) // 上一次的值
useIsFirstRender() // 是否首次渲染
useIsomorphicLayoutEffect() // SSR 安全的 useLayoutEffect
// ===== 表单类 =====
useForm(initialValues) // 或直接用 react-hook-form
useFieldArray()
// 几个高频 Hook 的典型实现
// ① useClickOutside
function useClickOutside(handler: () => void) {
const ref = useRef(null);
useEffect(() => {
const onClick = (e: MouseEvent) => {
if (!ref.current?.contains(e.target as Node)) handler();
};
document.addEventListener('pointerdown', onClick);
return () => document.removeEventListener('pointerdown', onClick);
}, [handler]);
return ref; // ⭐ 返回 ref 让调用方绑到元素上
}
// 使用
function Dropdown() {
const [open, setOpen] = useState(false);
const ref = useClickOutside(() => setOpen(false));
return ...;
}
// ② useIsFirstRender
function useIsFirstRender() {
const isFirst = useRef(true);
useEffect(() => {
isFirst.current = false;
}, []);
return isFirst.current;
}
// ③ usePrevious
function usePrevious(value: T): T | undefined {
const ref = useRef();
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}
// ④ useInterval(可动态调整间隔)
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useEffect(() => { savedCallback.current = callback; }, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
七、组合多个 Hook
// 自定义 Hook 可以调用其它自定义 Hook(组合是它的核心价值)
function useUserProfile(userId: string) {
// ① 请求数据
const { data: user, loading, error, refetch } = useFetch(`/api/users/${userId}`);
// ② 本地偏好
const [favorites, setFavorites] = useLocalStorage('favorites', []);
// ③ 计算派生值
const isFavorite = useMemo(
() => user != null && favorites.includes(user.id),
[user, favorites]
);
// ④ 跨标签页同步(订阅 storage 事件)
useEffect(() => {
const onStorage = (e: StorageEvent) => {
if (e.key === 'favorites') setFavorites(JSON.parse(e.newValue ?? '[]'));
};
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}, [setFavorites]);
const toggleFavorite = useCallback(() => {
if (!user) return;
setFavorites((prev) =>
prev.includes(user.id) ? prev.filter((id) => id !== user.id) : [...prev, user.id]
);
}, [user, setFavorites]);
return { user, loading, error, refetch, isFavorite, toggleFavorite };
}
// 使用:一个 Hook 聚合了一整块业务逻辑
function ProfilePage({ userId }: { userId: string }) {
const { user, loading, error, isFavorite, toggleFavorite } = useUserProfile(userId);
if (loading) return ;
if (error) return ;
if (!user) return null;
return (
{user.name}
);
}
八、常见坑
// ① 命名不以 use 开头 → lint 失效 + 语义不清
function fetchData() { const [d, setD] = useState(null); } // ❌
function useFetchData() { const [d, setD] = useState(null); } // ✅
// ② 有条件地调用自定义 Hook
if (cond) useSomething(); // ❌
// ③ 在自定义 Hook 内部有条件地调用 Hook
function useBad(cond: boolean) {
const [a] = useState(0);
if (cond) {
const [b] = useState(1); // ❌
}
}
// ④ 把「纯计算」抽成 Hook(没必要)
function useFullName(first: string, last: string) {
return `${first} ${last}`; // ❌ 没有 Hook,应该是普通函数
}
// ⑤ 忘记在内部清理副作用
function useBadEffect() {
useEffect(() => {
window.addEventListener('scroll', onScroll); // ❌ 没有清理
}, []);
}
// ⑥ 返回「每次渲染都是新对象」的结果,调用方放进依赖导致死循环
function useValue() {
return { data: someValue }; // ⚠️ 调用方放进依赖会出问题
}
// ⑦ 返回的函数不稳定 → 调用方的 effect 反复执行
function useHandler() {
return () => doSomething(); // ❌ 每次渲染都是新函数
// ✅ return useCallback(() => doSomething(), []);
}
// ⑧ 在 Hook 里用「每次渲染都新建的对象」作为依赖
useEffect(() => {}, [{ a: 1 }]); // ❌
// ⑨ 抽 Hook 抽得「过度」(一个 Hook 里塞了十几个 useState)
// → 按职责拆分
// ⑩ Hook 里依赖了调用方没传的东西(隐式耦合)
// → 通过参数显式传入(如 useFetch(url) 而不是读全局变量)
// ⑪ 认为「自定义 Hook 的状态是共享的」
// → 每个调用方有独立状态;要共享应该用 Context 或 store
// ⑫ 在 Hook 里返回 ref 却没有说明「要绑到哪个元素上」
// → 用 JSDoc 或类型说明(`@returns 需要绑定到目标元素的 ref`)
面试延伸
- 「自定义 Hook 是什么?」
自定义 Hook 是一个以use开头的函数,它内部可以调用其它 Hook,用来「复用有状态的逻辑」。关键点是:① 复用逻辑而不是状态——每个调用它的组件都有独立的 state;② 它只是普通函数(没有特殊语法),但必须遵守 Hook 规则(顶层调用);③ 命名必须以use开头,否则 ESLint 无法检查、语义也不清晰。 -
「自定义 Hook 和普通函数有什么区别?」
判断标准是「内部有没有用到 Hook」:① 用到了 Hook(useState/useEffect/useRef…)→ 自定义 Hook(可以保持跨渲染的状态、可以注册副作用);② 纯计算、无状态、无副作用→ 普通工具函数(更易测试、更轻量)。不要为了「看起来像 Hook」而把纯函数写进 Hook——那样只是多了一层函数调用。 -
「自定义 Hook 和组件有什么区别?」
接口不同:组件返回 JSX(React 元素),是「UI 的复用单位」;自定义 Hook 返回任意值(状态、方法、ref),是「逻辑的复用单位」。一个常见的组合是「用自定义 Hook 提供逻辑,用组件提供 UI」——这样同一套逻辑可以配不同的 UI(Headless UI 的思路)。 -
「为什么自定义 Hook 必须
use开头?」
两个原因:① ESLint 依赖命名识别——rules-of-hooks只检查use开头的函数内部的 Hook 调用;不这么命名会让 lint 形同虚设,隐藏 bug;② 语义约定——use前缀明确告诉调用者「这里有状态/副作用,必须遵守 Hook 规则(顶层调用)」。所以这是功能性要求,不只是风格。 -
「自定义 Hook 的状态是共享的吗?」
不是。每个调用它的组件都有独立的状态(因为useState的状态挂在调用它的组件实例上)。这与「全局 store」有本质区别:useToggle()在两个组件里各自独立,而 Zustand 的useStore()两个组件共享同一份数据。需要共享状态时应该用 Context 或状态管理库,而不是自定义 Hook。 -
「怎么设计一个好的自定义 Hook?」
六条经验:① 单一职责(一个 Hook 只做一件事);② 内部完成清理(定时器/监听/Observer 都由 Hook 负责,调用方不用操心);③ 返回稳定引用(函数用useCallback,避免调用方的依赖反复变化);④ 返回值风格按数量选(少用数组、多用对象);⑤ 参数显式(不要在 Hook 里读全局变量,通过参数传入);⑥ 纯逻辑不要抽成 Hook(没有 Hook 调用就用普通函数)。 -
「自定义 Hook 能做哪些典型的事?」
三类:① 状态封装(useToggle、useCounter、useLocalStorage、useForm);② 副作用协调(useDebounce、useInterval、useEventListener、useIntersectionObserver);③ 业务逻辑聚合(useUserProfile、useCart、usePermission——把「请求 + 本地状态 + 派生值 + 操作方法」打包成一块)。第三类是实际项目中最有价值的用法。 -
「怎么处理自定义 Hook 里
useEffect的依赖问题?」
三条经验:① Hook 内部的 Effect 依赖 Hook 的参数与内部状态(而不是外部变量);② 需要「读最新值但不重建订阅」时用useRef保存(useInterval、useEventListener里都是这个模式);③ 回调参数用 ref 保存(const fnRef = useRef(fn); useEffect(() => { fnRef.current = fn; })),这样就能在依赖数组里只写「真正需要重建的项」(如delay),避免调用方传入的内联函数导致反复重建。
一句话速记
自定义 Hook 是「复用有状态逻辑」的方式(复用逻辑不复用状态,每个调用方独立);命名必须
use开头(否则 lint 失效)、必须顶层调用(条件逻辑放进 Hook 内部);它和普通函数的边界是「内部有没有用 Hook」,和组件的边界是「返回 JSX 还是任意值」;设计要点是「单一职责 + 内部完成清理 + 返回稳定引用 + 参数显式」;高频清单里有useDebounce、useLocalStorage、useFetch、useEventListener、useClickOutside、useInterval等;要共享状态就用 Context/store,不要指望自定义 Hook。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!