自定义 Hooks 的真正威力在于将反复出现的业务逻辑抽离成可复用的函数。下面这些 Hooks 几乎会出现在每一个中大型 React 项目中,它们分别解决了数据请求、浏览器存储、防抖节流、状态切换等高频需求。
1. 数据请求:useRequest
手动在每个组件里写 useEffect + fetch 会导致大量重复的 loading、error 处理逻辑。封装一个通用的请求 Hook 可以统一管理异步请求的生命周期。
import { useState, useEffect, useCallback } from 'react';
function useRequest(requestFn, { immediate = true } = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const run = useCallback(async (...args) => {
setLoading(true);
setError(null);
try {
const result = await requestFn(...args);
setData(result);
return result;
} catch (err) {
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [requestFn]);
useEffect(() => {
if (immediate) {
run();
}
}, [immediate, run]);
return { data, loading, error, run };
}
// 使用
function UserList() {
const { data: users, loading, error } = useRequest(() =>
fetch('/api/users').then(res => res.json())
);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <List items={users} />;
}
要点:
- 通过
run方法支持手动触发(刷新、翻页等场景)。 requestFn通过参数传入,Hook 本身不耦合具体 API。- 实际项目中建议搭配 TanStack Query 处理更复杂的缓存与更新逻辑,但轻量场景下自封装
useRequest足够灵活。
2. 浏览器存储:useLocalStorage
把 localStorage 的读写同步到 React 状态,同时保证跨标签页的同步(可选)。
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
// 监听其他标签页的 storage 变化
useEffect(() => {
const handleStorageChange = (e) => {
if (e.key === key && e.newValue !== null) {
setStoredValue(JSON.parse(e.newValue));
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key]);
return [storedValue, setValue];
}
// 使用
const [theme, setTheme] = useLocalStorage('theme', 'light');
要点:
- 初始化时从
localStorage读取,避免每次渲染时 JSON 解析的性能开销。 - 支持函数式更新,保持与
useState一致的 API 风格。 storage事件监听实现了多标签页数据同步,适用于主题、登录状态等场景。
3. 防抖与节流:useDebounce / useThrottle
搜索框输入、窗口 resize 等高频触发场景,需要限制回调执行频率。
// 防抖:延迟执行,如果在延迟期间再次触发则重新计时
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 节流函数 Hook
function useThrottle(fn, delay = 200) {
const lastRun = useRef(Date.now());
return useCallback((...args) => {
const now = Date.now();
if (now - lastRun.current >= delay) {
fn(...args);
lastRun.current = now;
}
}, [fn, delay]);
}
// 使用
function SearchBox() {
const [keyword, setKeyword] = useState('');
const debouncedKeyword = useDebounce(keyword, 500);
useEffect(() => {
if (debouncedKeyword) {
fetchSearchResults(debouncedKeyword);
}
}, [debouncedKeyword]);
return <input onChange={e => setKeyword(e.target.value)} />;
}
要点:
useDebounce对值进行防抖,常用于搜索联想、表单校验等需要延迟触发的场景。useThrottle对函数进行节流,常用于滚动事件、拖拽等需要固定频率执行的场景。- 两者都通过清理
useEffect或useRef记录时间戳来避免内存泄漏和时间错乱。
4. 布尔切换:useToggle
看似简单,但写 setState(!state) 时往往会出现闭包陷阱。封装一个安全的切换 Hook 能避免很多低级错误。
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return [value, { toggle, setTrue, setFalse }];
}
// 使用
const [isOpen, { toggle, setFalse }] = useToggle(false);
return (
<>
<button onClick={toggle}>切换</button>
{isOpen && <Modal onClose={setFalse} />}
</>
);
要点:
toggle使用函数式更新setValue(v => !v),避免依赖外部状态值,杜绝闭包陷阱。- 返回的
setTrue/setFalse/toggle引用稳定,可以作为useEffect的依赖项安全传递。
5. 元素可见性:useIntersectionObserver
懒加载图片、下拉加载更多、曝光埋点等都需要检测某个 DOM 元素是否进入视口。借助 IntersectionObserver API,可以轻松封装。
function useIntersectionObserver(targetRef, options = {}) {
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
const target = targetRef.current;
if (!target) return;
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting);
}, options);
observer.observe(target);
return () => observer.disconnect();
}, [targetRef, options.threshold, options.rootMargin]);
return isIntersecting;
}
// 使用:懒加载图片
function LazyImage({ src, alt }) {
const imgRef = useRef(null);
const isVisible = useIntersectionObserver(imgRef, { threshold: 0.1 });
return (
<img
ref={imgRef}
src={isVisible ? src : ''}
alt={alt}
style={{ minHeight: 200, background: '#eee' }}
/>
);
}
要点:
- Hook 只关注“是否可见”这个单一状态,与具体业务逻辑解耦。
options支持配置触发阈值和根边距,适配不同提前量需求。- 观察器在组件卸载时自动断开,避免内存泄漏。
6. 异步操作状态封装:useAsync
很多场景下,我们只关心一个异步操作的 loading / error / data,并且需要手动触发。useAsync 比 useRequest 更轻量,适合表单提交、导出下载等非自动触发的场景。
function useAsync(asyncFn) {
const [state, setState] = useState({
status: 'idle', // idle | pending | success | error
data: null,
error: null,
});
const run = useCallback(async (...args) => {
setState({ status: 'pending', data: null, error: null });
try {
const data = await asyncFn(...args);
setState({ status: 'success', data, error: null });
return data;
} catch (error) {
setState({ status: 'error', error, data: null });
throw error;
}
}, [asyncFn]);
return { ...state, run };
}
// 使用:提交表单
const { status, error, run } = useAsync(submitForm);
return (
<form onSubmit={e => { e.preventDefault(); run(formData); }}>
<button disabled={status === 'pending'}>
{status === 'pending' ? '提交中...' : '提交'}
</button>
{status === 'error' && <p className="error">{error.message}</p>}
</form>
);
要点:
- 状态机设计避免了多个布尔值的混乱,
status明确表达当前阶段。 run返回 Promise,因此可以链式调用或者配合await,灵活处理后续操作。
封装原则总结
这几个示例覆盖了开发中 80% 的自定义 Hooks 场景。它们都遵循了以下原则:
- 单一职责:每个 Hook 只做一件事,命名清晰反映其功能。
- 状态与逻辑内聚:将相关的 state、effect 和业务逻辑封装在一起,对外暴露干净的 API。
- 与组件解耦:Hook 不依赖任何具体的组件上下文,可以在任何组件中使用。
- 通用性与可配置性:通过参数允许不同场景的配置,但不暴露不必要的实现细节。
掌握这些通用 Hooks 的封装模式后,面对新的业务需求时,你自然能够快速抽离出可复用的逻辑,让代码库始终保持干净、可维护。