ahooks
是阿里开源的一套 高质量 React Hooks 库 。它的价值不只在"省代码",更在于把常见的业务模式(数据请求、状态管理、节流防抖、可见性检测、持久化等)封装为可复用、可测试、类型友好的能力,从而让我们把精力放在业务本身。
下面我用 TypeScript + Ant Design 的例子,从"为什么用、怎么用、最佳实践与坑点"三个方面讲清楚。
一、为什么是 ahooks?
- 减少样板代码 :一次封装的
useRequest
替代了 axios + loading + try/catch + 取消请求 + 轮询/重试 + 缓存的重复劳动。 - 一致的团队范式:统一用法、统一错误处理、统一数据流,项目可维护性显著提升。
- 类型完善:多数 Hook 都有完善的 TS 定义,配合接口类型推导非常顺手。
- 与 Ant Design 生态契合 :
useAntdTable
、useSelections
、usePagination
天然适配常见的管理后台场景。
二、核心能力与场景化示例
1. 数据请求:useRequest
(项目里使用频率最高)
1.1 基础用法
typescript
import { useRequest } from 'ahooks';
import { message } from 'antd';
interface User { id: number; name: string }
interface ListRes { list: User[]; total: number }
async function fetchUsers(params: { current: number; pageSize: number; keyword?: string }): Promise<ListRes> {
const res = await fetch('/api/users?' + new URLSearchParams(params as any));
if (!res.ok) throw new Error('Network error');
return res.json();
}
export default function UserList() {
const { data, loading, error, run } = useRequest(fetchUsers, {
manual: true, // 手动触发
onError: (e) => message.error(e.message),
});
useEffect(() => {
run({ current: 1, pageSize: 10 });
}, [run]);
if (error) return <div>加载失败</div>;
if (loading) return <div>加载中...</div>;
return <pre>{JSON.stringify(data?.list, null, 2)}</pre>;
}
1.2 进阶:分页 + 搜索 + 自动依赖刷新 + 缓存 + 防抖/节流
php
const { data, loading, run, params, refresh } = useRequest(fetchUsers, {
manual: true,
// 防抖 or 节流:避免频繁请求
debounceWait: 300,
// throttleWait: 300,
// 依赖变化自动刷新,例如 keyword 改变
refreshDeps: [keyword, page.current, page.pageSize],
// 缓存:同样的参数 5 分钟内命中缓存
cacheKey: 'users:list',
staleTime: 1000 * 60 * 5,
// 失败自动重试 3 次
retryCount: 3,
});
1.3 轮询/长连接替代、条件开启与停止
javascript
const canPoll = status === 'running';
const { cancel } = useRequest(fetchProgress, {
ready: canPoll, // 只有条件满足才开始
pollingInterval: 5000, // 每 5s 轮询
onSuccess: (res) => {
if (res.done) cancel(); // 任务完成后停止轮询
},
});
1.4 依赖请求 & 乐观更新
php
// 依赖请求:拿到 userId 再拉取订单
const userReq = useRequest(getUser, { manual: true });
const orderReq = useRequest(() => getOrders(userReq.data!.id), {
ready: !!userReq.data, // 等 userId 有值
});
// 乐观更新:先更新 UI,再回滚或确认
const { run: updateName, mutate } = useRequest(patchUserName, { manual: true });
const onRename = async (name: string) => {
const snapshot = userReq.data;
mutate({ ...snapshot!, name }); // 乐观更新
try {
await updateName({ id: snapshot!.id, name });
} catch {
mutate(snapshot!); // 失败回滚
}
};
2. 表格一条龙:useAntdTable
把「表单搜索 + 分页表格 + 请求映射」合成固定套路,极大减少模板代码。
typescript
import { Table, Form, Input } from 'antd';
import { useAntdTable } from 'ahooks';
interface TableItem { id: number; name: string }
interface TableParams { current: number; pageSize: number; keyword?: string }
interface TableResult { list: TableItem[]; total: number }
async function tableService({ current, pageSize }: TableParams, formData: any): Promise<TableResult> {
const res = await fetch('/api/users?' + new URLSearchParams({ current, pageSize, ...formData }));
return res.json();
}
export default function Users() {
const [form] = Form.useForm();
const { tableProps, search } = useAntdTable(tableService, {
form,
defaultParams: [{ current: 1, pageSize: 10 }],
});
return (
<>
<Form form={form} layout="inline" onFinish={search.submit}>
<Form.Item name="keyword" label="关键词"><Input allowClear /></Form.Item>
<Form.Item><button onClick={() => search.submit()}>查询</button></Form.Item>
<Form.Item><button onClick={() => search.reset()}>重置</button></Form.Item>
</Form>
<Table rowKey="id" {...tableProps} />
</>
);
}
3. 状态开关:useBoolean
(弹窗、按钮禁用、开关逻辑)
javascript
import { useBoolean } from 'ahooks';
const [visible, { setTrue: open, setFalse: close, toggle }] = useBoolean(false);
<Button onClick={open}>新建</Button>
<Modal open={visible} onCancel={close} onOk={close} />
4. 性能与闭包安全:useMemoizedFn
/ useLatest
React 中最容易踩坑的是 陈旧闭包(stale closure) 。ahooks
给了更安全的替代方案。
javascript
import { useMemoizedFn, useLatest } from 'ahooks';
const latest = useLatest(state); // 始终拿到最新值
const handleClick = useMemoizedFn(() => { // 永不变引用,但拿到最新闭包
console.log(latest.current);
});
何时用?
- 事件回调传给子组件、addEventListener、setInterval 等需要稳定引用 同时又要拿到最新状态的场景。
5. 输入优化:useDebounceFn
/ useThrottleFn
/ useDebounce
/ useThrottle
搜索框输入 300ms 后请求:
typescript
import { useDebounceFn } from 'ahooks';
const { run: searchRun } = useDebounceFn((value: string) => {
run({ current: 1, pageSize: 10, keyword: value }); // 触发 useRequest
}, { wait: 300 });
<Input onChange={e => searchRun(e.target.value)} allowClear />
6. 选择集合:useSelections
(批量勾选管理超级好用)
ini
import { useSelections } from 'ahooks';
const [ids, setIds] = useState<string[]>([]);
const { selected, allSelected, isSelected, toggle, toggleAll, partiallySelected } =
useSelections(ids, []);
<Table
rowKey="id"
rowSelection={{
selectedRowKeys: selected,
onChange: (_, rows) => toggleAll(rows.map(r => r.id)),
}}
/>
7. 可见性与交互:useInViewport
/ useClickAway
/ useEventListener
csharp
const ref = useRef(null);
const [inView] = useInViewport(ref);
<div ref={ref}>{inView ? '我在视口内' : '我不在视口内'}</div>
// 点击组件外关闭浮层
useClickAway(() => setOpen(false), ref);
8. 持久化状态:useLocalStorageState
/ useSessionStorageState
arduino
const [theme, setTheme] = useLocalStorageState<'light' | 'dark'>('app-theme', {
defaultValue: 'light',
});
9. 无限滚动:useInfiniteScroll
javascript
const { data, loading, loadMore, noMore } = useInfiniteScroll(
(d) => fetchPage({ page: d?.page! + 1 || 1 }),
{ isNoMore: d => (d ? d.page >= d.totalPages : false) }
);
10. 实时通信:useWebSocket
scss
const { latestMessage, sendMessage, readyState } = useWebSocket('wss://echo.websocket.org');
useEffect(() => {
if (latestMessage) console.log(latestMessage.data);
}, [latestMessage]);
sendMessage('hello');
三、把 ahooks 融到你的"项目基建"
1. 统一的请求层
typescript
// request.ts
export async function request<T>(url: string, opts?: RequestInit): Promise<T> {
const res = await fetch(url, opts);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
// services/user.ts
export const UserAPI = {
list: (p: { current: number; pageSize: number; keyword?: string }) =>
request<{ list: User[]; total: number }>('/api/users?' + new URLSearchParams(p as any)),
};
搭配 useRequest
:
php
const listReq = useRequest(UserAPI.list, {
manual: true,
cacheKey: 'user:list',
staleTime: 60_000,
retryCount: 2,
onError: e => message.error(e.message),
});
2. 统一的错误提示 & 权限判断
- 在
onError
里弹 toast。 - 用
ready
或manual
+ 自己的鉴权 Hook 控制是否发起请求。
3. 组件通信与状态
- 全局轻量状态可用
useLocalStorageState
/useBoolean + context
。 - 需要稳定回调的地方统一用
useMemoizedFn
,避免因为useCallback
依赖变化导致子组件频繁重渲染。
四、最佳实践清单
- 能用
useRequest
就不用自己封一层 axios hooks------它的插件位(防抖/节流/缓存/重试/轮询/依赖刷新)经不起重复造轮子。 - 事件/定时器/订阅回调统一用
useMemoizedFn
+useLatest
,远离陈旧闭包。 - 表格页优先选
useAntdTable
,自带表单 + 分页联动,减少胶水代码。 - 用户输入引发请求,务必配
useDebounceFn
。 - 弹窗/抽屉/开关统一用
useBoolean
,读起来一目了然。 - 需要持久化的 UI 偏好(主题、列宽、搜索条件)用
useLocalStorageState
。 - 轮询要 及时
cancel
;依赖请求要用ready
和refreshDeps
。 - 给
useRequest
的service
总是 返回 Promise 且具备明确类型,让 TS 为你兜底。 - 对外暴露只读数据时,优先
const { data } = useRequest(...)
,避免在组件外部再维护多份状态。
五、常见坑与规避
- 闭包问题 :
setInterval
里拿到的 state 是旧的?→useLatest
oruseMemoizedFn
。 - 过度重渲染 :传入子组件的回调每次变?→ 用
useMemoizedFn
替换useCallback
。 - 重复请求 :输入框触发请求过于频繁?→
useDebounceFn
/debounceWait
。 - 内存泄漏 :轮询/订阅没有清理?→
useRequest.cancel
、useEventListener
会自动清理,或在useEffect
里 return 清理函数。 - 缓存陷阱 :
cacheKey + staleTime
使用不当导致数据不更新?→ 调整staleTime
,必要时用refresh
或修改params
触发新请求。
六、一个完整的小页面示例(搜索 + 表格 + 弹窗编辑)
typescript
import { Table, Form, Input, Modal, Button, message } from 'antd';
import { useAntdTable, useBoolean, useMemoizedFn, useRequest } from 'ahooks';
import { UserAPI } from '@/services/user';
interface User { id: number; name: string; email: string }
export default function UserPage() {
const [form] = Form.useForm();
const [visible, { setTrue: open, setFalse: close }] = useBoolean(false);
const [current, setCurrent] = useState<User | null>(null);
// 列表
const { tableProps, search, refresh } = useAntdTable(
(p, f) => UserAPI.list({ current: p.current, pageSize: p.pageSize, ...f }),
{ form, defaultParams: [{ current: 1, pageSize: 10 }] }
);
// 保存
const { run: saveUser, loading: saving } = useRequest(UserAPI.save, {
manual: true,
onSuccess: () => { message.success('保存成功'); close(); refresh(); },
});
const onEdit = useMemoizedFn((record: User) => {
setCurrent(record);
open();
});
return (
<>
<Form form={form} layout="inline" onFinish={search.submit}>
<Form.Item name="keyword" label="搜索"><Input allowClear /></Form.Item>
<Button type="primary" onClick={() => search.submit()}>查询</Button>
<Button onClick={() => search.reset()}>重置</Button>
</Form>
<Table
rowKey="id"
{...tableProps}
columns={[
{ title: 'ID', dataIndex: 'id' },
{ title: '姓名', dataIndex: 'name' },
{ title: '邮箱', dataIndex: 'email' },
{ title: '操作', render: (_, r) => <Button onClick={() => onEdit(r)}>编辑</Button> },
]}
/>
<Modal title="编辑用户" open={visible} onCancel={close} onOk={() => saveUser(current!)} confirmLoading={saving}>
{current?.name}
</Modal>
</>
);
}
这个页面里我们用到了:
useAntdTable
:统一管理表单/分页/请求useBoolean
:管理弹窗显隐useRequest
:提交保存,成功后刷新列表useMemoizedFn
:把回调固定且拿到最新current
结语
ahooks
并不是"另一个工具库",它更像是 把前端业务的 80% 常见模式做了抽象。用它,可以让你的代码:
- 更短(少一半代码量不是梦)
- 更稳(少错、少漏清理、少闭包坑)
- 更统一(团队成员无缝协作)
如果你正在做 React/AntD 的中后台或 B 端应用,ahooks
值得成为你项目的基建之一 。如果你愿意,我可以根据你的项目结构,把 useRequest
、错误处理、鉴权、缓存策略等抽一套可直接落地的脚手架模板给你。