告别重复造轮子!看 ahooks 如何改变你的代码结构

ahooks 是阿里开源的一套 高质量 React Hooks 库 。它的价值不只在"省代码",更在于把常见的业务模式(数据请求、状态管理、节流防抖、可见性检测、持久化等)封装为可复用、可测试、类型友好的能力,从而让我们把精力放在业务本身。

下面我用 TypeScript + Ant Design 的例子,从"为什么用、怎么用、最佳实践与坑点"三个方面讲清楚。


一、为什么是 ahooks?

  • 减少样板代码 :一次封装的 useRequest 替代了 axios + loading + try/catch + 取消请求 + 轮询/重试 + 缓存的重复劳动。
  • 一致的团队范式:统一用法、统一错误处理、统一数据流,项目可维护性显著提升。
  • 类型完善:多数 Hook 都有完善的 TS 定义,配合接口类型推导非常顺手。
  • 与 Ant Design 生态契合useAntdTableuseSelectionsusePagination 天然适配常见的管理后台场景。

二、核心能力与场景化示例

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。
  • readymanual + 自己的鉴权 Hook 控制是否发起请求。

3. 组件通信与状态

  • 全局轻量状态可用 useLocalStorageState / useBoolean + context
  • 需要稳定回调的地方统一用 useMemoizedFn,避免因为 useCallback 依赖变化导致子组件频繁重渲染。

四、最佳实践清单

  1. 能用 useRequest 就不用自己封一层 axios hooks------它的插件位(防抖/节流/缓存/重试/轮询/依赖刷新)经不起重复造轮子。
  2. 事件/定时器/订阅回调统一用 useMemoizedFn + useLatest,远离陈旧闭包。
  3. 表格页优先选 useAntdTable,自带表单 + 分页联动,减少胶水代码。
  4. 用户输入引发请求,务必配 useDebounceFn
  5. 弹窗/抽屉/开关统一用 useBoolean,读起来一目了然。
  6. 需要持久化的 UI 偏好(主题、列宽、搜索条件)用 useLocalStorageState
  7. 轮询要 及时 cancel ;依赖请求要用 readyrefreshDeps
  8. useRequestservice 总是 返回 Promise具备明确类型,让 TS 为你兜底。
  9. 对外暴露只读数据时,优先 const { data } = useRequest(...),避免在组件外部再维护多份状态。

五、常见坑与规避

  • 闭包问题setInterval 里拿到的 state 是旧的?→ useLatest or useMemoizedFn
  • 过度重渲染 :传入子组件的回调每次变?→ 用 useMemoizedFn 替换 useCallback
  • 重复请求 :输入框触发请求过于频繁?→ useDebounceFn / debounceWait
  • 内存泄漏 :轮询/订阅没有清理?→ useRequest.canceluseEventListener 会自动清理,或在 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、错误处理、鉴权、缓存策略等抽一套可直接落地的脚手架模板给你。

相关推荐
中国lanwp3 小时前
Tomcat 中部署 Web 应用
java·前端·tomcat
袁煦丞3 小时前
WSL双系统协作神器:cpolar内网穿透实验室第517个成功挑战
前端·程序员·远程工作
起风了啰3 小时前
SkySwitch 云控灯
前端
大力yy3 小时前
从零到一:VS Code 扩展开发全流程简介(含 Webview 与 React 集成)
前端·javascript·react.js
Deepsleep.3 小时前
前端常见安全问题 + 防御方法 + 面试回答
前端·安全·面试
IT小农工3 小时前
windows系统edge浏览器退出账户后还能免密登录
前端·edge
柯南95273 小时前
Chrome浏览器插件(Extensions)的原理
前端·chrome
小妖6663 小时前
如何去除edge浏览器的灰色边框
前端·edge
猪哥帅过吴彦祖3 小时前
JavaScript Set 和 Map:现代 JavaScript 的数据结构双雄
前端·javascript·面试