Mastering ahooks: Practical Tips and Full Guide for React Developers
This article provides a comprehensive, step‑by‑step guide to using Alibaba's ahooks library with React 18, covering installation, core hooks like useRequest, usePagination, state management hooks, side‑effect utilities, performance optimizations, real‑world examples, common pitfalls and best‑practice recommendations.
Quick Start
Install the library: npm install ahooks ahooks supports on‑demand import and is tree‑shaking friendly, so it does not add unnecessary bundle size.
Network Requests – useRequest
Basic usage
import { useRequest } from 'ahooks';
import request from '~/req';
interface UserInfo {
id: string;
name: string;
email: string;
}
const fetchUser = async (id: string): Promise<UserInfo> => {
const res = await request.get(`/api/user/${id}`);
return res.data;
};
const UserDetail: React.FC<{ userId: string }> = ({ userId }) => {
const { data, loading, error } = useRequest(() => fetchUser(userId));
if (loading) return <Spin />;
if (error) return <div>请求失败:{error.message}</div>;
return <div>{data?.name}</div>;
};Manual trigger
const { loading, run } = useRequest(fetchUser, {
manual: true,
});
<Button loading={loading} onClick={() => run('user-id-123')}>查询用户</Button>Refresh on dependency change (refreshDeps)
const [userId, setUserId] = useState('1');
const { data } = useRequest(() => fetchUser(userId), {
refreshDeps: [userId], // userId changes → request re‑runs
});Polling request
const { data } = useRequest(fetchSystemStatus, {
pollingInterval: 3000,
pollingWhenHidden: false, // pause when page hidden
});Debounce & throttle
// Debounce search (500 ms)
const { run } = useRequest(fetchSearchResults, {
manual: true,
debounceWait: 500,
});
<Input onChange={e => run(e.target.value)} />
// Throttle (once per second)
const { run } = useRequest(fetchData, {
manual: true,
throttleWait: 1000,
});Cache & SWR strategy
const { data } = useRequest(fetchUser, {
cacheKey: `user-${userId}`,
staleTime: 5 * 60 * 1000, // 5 min cache
});Error retry
const { data, error } = useRequest(fetchData, {
retryCount: 3,
retryInterval: 1000,
});Request lifecycle callbacks
const { run } = useRequest(submitForm, {
manual: true,
onBefore: params => console.log('请求开始', params),
onSuccess: (data, params) => message.success('提交成功'),
onError: error => message.error(`提交失败:${error.message}`),
onFinally: () => {/* always runs */},
});Pagination – usePagination
import { usePagination } from 'ahooks';
interface PageParams { current: number; pageSize: number; }
interface UserListResult { total: number; list: UserInfo[]; }
const fetchUserList = async (params: PageParams): Promise<UserListResult> => {
const res = await request.get('/api/users', { params });
return res.data;
};
const UserList: React.FC = () => {
const { data, loading, pagination } = usePagination(fetchUserList, {
defaultPageSize: 10,
});
return (
<>
<Table
dataSource={data?.list}
loading={loading}
pagination={{
total: data?.total,
current: pagination.current,
pageSize: pagination.pageSize,
onChange: pagination.onChange,
}}
/>
</>
);
};Infinite scroll – useInfiniteScroll
import { useInfiniteScroll } from 'ahooks';
interface InfiniteListResult { list: UserInfo[]; nextCursor?: string; }
const fetchInfiniteList = async (d?: InfiniteListResult): Promise<InfiniteListResult> => {
const res = await request.get('/api/users', { params: { cursor: d?.nextCursor } });
return res.data;
};
const InfiniteList: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const { data, loadingMore, loadMore, noMore } = useInfiniteScroll(fetchInfiniteList, {
target: containerRef,
isNoMore: d => !d?.nextCursor,
});
return (
<div ref={containerRef} style={{ overflow: 'auto', height: 400 }}>
{data?.list.map(item => <div key={item.id}>{item.name}</div>)}
{loadingMore && <Spin />}
{noMore && <div>没有更多了</div>}
</div>
);
};State Management Hooks
useToggle – boolean toggle
import { useToggle } from 'ahooks';
const Modal: React.FC = () => {
const [visible, { toggle, setLeft: hide, setRight: show }] = useToggle(false);
return (
<>
<Button onClick={show}>打开弹窗</Button>
<Dialog open={visible} onClose={hide}>内容</Dialog>
</>
);
};useBoolean – semantic boolean
import { useBoolean } from 'ahooks';
const [loading, { setTrue: startLoading, setFalse: stopLoading }] = useBoolean(false);
const handleSubmit = async () => {
startLoading();
try { await submitData(); } finally { stopLoading(); }
};useSet & useMap – collections
import { useSet, useMap } from 'ahooks';
// useSet
const [selectedIds, { add, remove, has, reset }] = useSet<string>();
const toggleRow = (id: string) => {
has(id) ? remove(id) : add(id);
};
// useMap
const [cache, { set: setCache, get: getCache, remove: removeCache }] = useMap<string, any>();useLocalStorageState – persistence
import { useLocalStorageState } from 'ahooks';
const [theme, setTheme] = useLocalStorageState<'light' | 'dark'>('app-theme', {
defaultValue: 'light',
serializer: JSON.stringify,
deserializer: JSON.parse,
});useUrlState – sync with URL
import { useUrlState } from 'ahooks';
const [state, setState] = useUrlState({ page: '1', keyword: '', status: 'active' });
// URL becomes ?page=1&keyword=&status=active
setState({ page: '2', keyword: 'john' });Side‑Effect Hooks
useDebounceEffect – debounced side effect
import { useDebounceEffect } from 'ahooks';
useDebounceEffect(() => {
if (keyword) fetchSearchResults(keyword);
}, [keyword], { wait: 500 });useThrottleEffect – throttled side effect
import { useThrottleEffect } from 'ahooks';
useThrottleEffect(() => {
console.log('scrollTop:', window.scrollY);
}, [scrollY], { wait: 200 });useDeepCompareEffect – deep compare deps
import { useDeepCompareEffect } from 'ahooks';
useDeepCompareEffect(() => {
fetchData(filters);
}, [filters]);useUpdateEffect – skip first run
import { useUpdateEffect } from 'ahooks';
useUpdateEffect(() => {
message.info('筛选条件已更新');
}, [filters]);useAsyncEffect – async side effect
import { useAsyncEffect } from 'ahooks';
useAsyncEffect(async () => {
const data = await fetchInitData();
setInitData(data);
}, []);Performance Optimization Hooks
useDebounceFn & useThrottleFn – debounced / throttled functions
import { useDebounceFn, useThrottleFn } from 'ahooks';
const { run: debouncedSearch } = useDebounceFn((keyword: string) => {
fetchSearchResults(keyword);
}, { wait: 300 });
const { run: throttledScroll } = useThrottleFn(() => {
updateScrollPosition();
}, { wait: 100 });useDebounceValue & useThrottleValue – debounced / throttled values
import { useDebounceValue, useThrottleValue } from 'ahooks';
const [inputValue, setInputValue] = useState('');
const debouncedValue = useDebounceValue(inputValue, { wait: 500 });
useEffect(() => {
if (debouncedValue) fetchResults(debouncedValue);
}, [debouncedValue]);useMemoizedFn – stable function reference
import { useMemoizedFn } from 'ahooks';
const handleSubmit = useMemoizedFn(async (values: FormValues) => {
const result = await submitData({ ...values, userId });
onSuccess(result);
});
<MemoizedForm onSubmit={handleSubmit} />DOM Hooks
useEventListener – event binding
import { useEventListener } from 'ahooks';
useEventListener('keydown', e => {
if (e.key === 'Escape') closeModal();
});
const buttonRef = useRef<HTMLButtonElement>(null);
useEventListener('click', handleClick, { target: buttonRef });useInViewport – visibility detection
import { useInViewport } from 'ahooks';
const ref = useRef<HTMLDivElement>(null);
const [inViewport] = useInViewport(ref);
return (
<div ref={ref}>
{inViewport ? <img src={imageSrc} alt="" /> : <Skeleton />}
</div>
);useSize – element size
import { useSize } from 'ahooks';
const containerRef = useRef<HTMLDivElement>(null);
const size = useSize(containerRef);
const columns = size?.width && size.width > 800 ? 3 : 1;useScroll – scroll position
import { useScroll } from 'ahooks';
const position = useScroll(document);
const showBackTop = (position?.top ?? 0) > 300;useTextSelection – text selection
import { useTextSelection } from 'ahooks';
const { text, left, top } = useTextSelection();
return (
<>
<article>长文章内容...</article>
{text && (
<div style={{ position: 'fixed', left, top }}>
<Button onClick={() => translate(text)}>翻译</Button>
<Button onClick={() => copy(text)}>复制</Button>
</div>
)}
</>
);useFullscreen – full‑screen control
import { useFullscreen } from 'ahooks';
const videoRef = useRef<HTMLDivElement>(null);
const [isFullscreen, { enterFullscreen, exitFullscreen, toggleFullscreen }] =
useFullscreen(videoRef);
<Button onClick={toggleFullscreen}>
{isFullscreen ? '退出全屏' : '全屏'}
</Button>useDrop & useDrag – drag‑and‑drop
import { useDrop, useDrag } from 'ahooks';
const DropZone: React.FC = () => {
const [isHovering, setIsHovering] = useState(false);
const [props] = useDrop({
onFiles: files => uploadFiles(files),
onText: text => console.log('拖入文本:', text),
onDragEnter: () => setIsHovering(true),
onDragLeave: () => setIsHovering(false),
});
return (
<div {...props} style={{ border: isHovering ? '2px dashed blue' : '2px dashed gray' }}>
拖拽文件到此处
</div>
);
};Timer Hooks
useInterval – recurring timer
import { useInterval } from 'ahooks';
const [count, setCount] = useState(0);
useInterval(() => setCount(c => c + 1), 1000);useTimeout – delayed execution
import { useTimeout } from 'ahooks';
useTimeout(() => setVisible(false), 3000);useCountDown – countdown
import { useCountDown } from 'ahooks';
const [countdown, formattedRes] = useCountDown({ targetDate: targetTime });
const { seconds } = formattedRes;
<Button disabled={seconds > 0} onClick={sendCode}>
{seconds > 0 ? `${seconds}s 后重发` : '发送验证码'}
</Button>Practical Cases
Case 1 – Debounced search table
import { useRequest, useDebounceFn, usePagination } from 'ahooks';
import { useState } from 'react';
interface SearchParams { keyword: string; current: number; pageSize: number; }
const fetchTableData = async (params: SearchParams) => {
const res = await request.get('/api/table', { params });
return res.data;
};
const DataTable: React.FC = () => {
const [keyword, setKeyword] = useState('');
const [searchKeyword, setSearchKeyword] = useState('');
const { run: debouncedSetKeyword } = useDebounceFn(
(value: string) => setSearchKeyword(value),
{ wait: 500 }
);
const { data, loading, pagination } = usePagination(
({ current, pageSize }) => fetchTableData({ keyword: searchKeyword, current, pageSize }),
{ refreshDeps: [searchKeyword] }
);
return (
<>
<Input
value={keyword}
onChange={e => { setKeyword(e.target.value); debouncedSetKeyword(e.target.value); }}
placeholder="搜索..."
/>
<Table
dataSource={data?.list}
loading={loading}
pagination={{
total: data?.total,
current: pagination.current,
pageSize: pagination.pageSize,
onChange: pagination.onChange,
}}
/>
</>
);
};Case 2 – Offline‑aware data sync
import { useNetwork, useRequest, useInterval } from 'ahooks';
const SyncStatus: React.FC = () => {
const { online } = useNetwork();
const [lastSynced, setLastSynced] = useState<Date | null>(null);
const { run: syncData } = useRequest(syncToServer, {
manual: true,
onSuccess: () => setLastSynced(new Date()),
});
useInterval(() => { if (online) syncData(); }, online ? 30000 : null);
return (
<div>
{online ? '🟢 在线' : '🔴 离线'}
{lastSynced && ` · 上次同步:${lastSynced.toLocaleTimeString()}`}
</div>
);
};Case 3 – Undoable rich‑text editor
import { useHistoryTravel, useKeyPress, useMemoizedFn } from 'ahooks';
const RichEditor: React.FC = () => {
const { value, setValue, back, forward, backLength, forwardLength } =
useHistoryTravel<string>('');
const handleChange = useMemoizedFn((content: string) => setValue(content));
useKeyPress(['ctrl.z', 'meta.z'], e => { e.preventDefault(); if (backLength > 0) back(); });
useKeyPress(['ctrl.shift.z', 'meta.shift.z'], e => { e.preventDefault(); if (forwardLength > 0) forward(); });
return (
<div>
<Toolbar>
<Button disabled={backLength === 0} onClick={() => back()}>撤销 ({backLength})</Button>
<Button disabled={forwardLength === 0} onClick={() => forward()}>重做 ({forwardLength})</Button>
</Toolbar>
<TextArea value={value} onChange={e => handleChange(e.target.value)} />
</div>
);
};Case 4 – Virtual list with infinite scroll
import { useInfiniteScroll, useInViewport } from 'ahooks';
import { useRef } from 'react';
const fetchPage = async (d?: { nextCursor: string; list: Post[] }) => {
const res = await request.get('/api/posts', { params: { cursor: d?.nextCursor } });
return res.data as { list: Post[]; nextCursor: string };
};
const Feed: React.FC = () => {
const listRef = useRef<HTMLDivElement>(null);
const footerRef = useRef<HTMLDivElement>(null);
const [footerVisible] = useInViewport(footerRef);
const { data, loadingMore, loadMore, noMore } = useInfiniteScroll(fetchPage, {
target: listRef,
isNoMore: d => !d?.nextCursor,
});
useEffect(() => {
if (footerVisible && !loadingMore && !noMore) loadMore();
}, [footerVisible]);
return (
<div ref={listRef} style={{ height: '100vh', overflow: 'auto' }}>
{data?.list.map(post => <PostCard key={post.id} post={post} />)}
<div ref={footerRef}>
{loadingMore && <Spin />}
{noMore && <div>已经到底了</div>}
</div>
</div>
);
};Common Pitfalls & Best Practices
1️⃣ Keep the service function stable
// ❌ Bad: creates a new function each render → endless loop
const { data } = useRequest(async () => {
return request.get(`/api/user/${userId}`);
});
// ✅ Good: pass dependencies via refreshDeps
const { data } = useRequest(
() => request.get(`/api/user/${userId}`).then(r => r.data),
{ refreshDeps: [userId] }
);2️⃣ Prefer useMemoizedFn over useCallback
// ❌ useCallback needs a long dependency list
const handleClick = useCallback(() => doSomething(a, b, c), [a, b, c]);
// ✅ useMemoizedFn gives a stable reference and always reads latest values
const handleClick = useMemoizedFn(() => doSomething(a, b, c));3️⃣ Serialize complex objects with useLocalStorageState
const [config, setConfig] = useLocalStorageState<AppConfig>('app-config', {
defaultValue: DEFAULT_CONFIG,
serializer: v => JSON.stringify(v ?? null),
deserializer: v => {
try { return JSON.parse(v) ?? DEFAULT_CONFIG; }
catch { return DEFAULT_CONFIG; }
},
});4️⃣ Fix target time for useCountDown
// ❌ Re‑calculating targetDate each render resets the timer
const [cd] = useCountDown({ targetDate: Date.now() + 60000 });
// ✅ Store the target time in a ref or state
const [targetTime] = useState(() => Date.now() + 60000);
const [cd] = useCountDown({ targetDate: targetTime });5️⃣ Combine pollingInterval with page visibility
const { data } = useRequest(fetchLiveData, {
pollingInterval: 5000,
pollingWhenHidden: false, // pause when tab hidden
pollingErrorRetryCount: 3, // stop after 3 consecutive errors
});Conclusion
ahooks follows a design philosophy of "out‑of‑the‑box usability and business‑logic focus". By leveraging the hooks shown above, developers can dramatically reduce boilerplate, keep component logic clear, and enjoy type safety and performance guarantees. All examples assume ahooks 3.x with TypeScript.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
