287 lines
8.9 KiB
Markdown
287 lines
8.9 KiB
Markdown
|
|
# Ahooks 使用指南(基础、进阶与常见用法)
|
|||
|
|
|
|||
|
|
Ahooks 是一套高质量、可复用的 React Hooks 集合,适合在业务工程中快速构建稳定的交互与数据逻辑。本文档覆盖快速上手、基础用法、`useRequest` 核心与进阶、常见场景模板、最佳实践与坑位排查,结合本项目技术栈(React 18 + Vite + MUI)。
|
|||
|
|
|
|||
|
|
目录
|
|||
|
|
- 为什么选 Ahooks
|
|||
|
|
- 安装与类型支持
|
|||
|
|
- 快速上手:页面标题(`useTitle`)
|
|||
|
|
- 基础能力:状态/事件/定时/存储/性能
|
|||
|
|
- useRequest 基础与进阶(自动/手动、轮询、缓存、SWR、重试等)
|
|||
|
|
- 常见场景模板(搜索防抖、任务轮询、分页、依赖请求、自动保存)
|
|||
|
|
- 最佳实践与常见坑位
|
|||
|
|
- 速查表与参考资料
|
|||
|
|
|
|||
|
|
## 为什么选用 Ahooks
|
|||
|
|
|
|||
|
|
- 大量生产验证的 Hooks,覆盖状态管理、请求、DOM、浏览器、性能优化等场景。
|
|||
|
|
- API 统一、可组合,适合封装成业务级自定义 Hook。
|
|||
|
|
- 降低维护成本:内置防抖/节流、轮询、缓存、并发控制、卸载安全、SWR 等复杂能力。
|
|||
|
|
|
|||
|
|
## 安装与类型支持
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
pnpm add ahooks
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
TypeScript 用户无需额外配置,Ahooks 提供完整的类型提示。
|
|||
|
|
|
|||
|
|
## 快速上手:更新页面标题(useTitle)
|
|||
|
|
|
|||
|
|
最直接的需求就是“更新页面标题”。`useTitle` 在组件挂载时设置 `document.title`,卸载时可恢复。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import { useTitle } from 'ahooks';
|
|||
|
|
|
|||
|
|
export default function KnowledgeListPage() {
|
|||
|
|
useTitle('TERES · 知识库列表', { restoreOnUnmount: true });
|
|||
|
|
return (<div>...</div>);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
如需统一加前后缀,可封装:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import { useTitle } from 'ahooks';
|
|||
|
|
|
|||
|
|
export function usePageTitle(title?: string, opts?: { prefix?: string; suffix?: string }) {
|
|||
|
|
const final = [opts?.prefix, title, opts?.suffix].filter(Boolean).join(' · ');
|
|||
|
|
useTitle(final || 'TERES', { restoreOnUnmount: true });
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 基础能力(常用 Hooks)
|
|||
|
|
|
|||
|
|
- useBoolean / useToggle:布尔与枚举状态切换
|
|||
|
|
```tsx
|
|||
|
|
const [visible, { setTrue, setFalse, toggle }] = useBoolean(false);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- useDebounceFn / useThrottleFn:防抖与节流(输入、滚动、窗口变化)
|
|||
|
|
```tsx
|
|||
|
|
const { run: onSearchChange, cancel } = useDebounceFn((kw: string) => setKeywords(kw), { wait: 300 });
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- useEventListener:声明式事件监听(自动清理)
|
|||
|
|
```tsx
|
|||
|
|
useEventListener('resize', () => console.log(window.innerWidth), { target: window });
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- useLocalStorageState / useSessionStorageState:状态持久化
|
|||
|
|
```tsx
|
|||
|
|
const [lang, setLang] = useLocalStorageState('lang', { defaultValue: 'zh' });
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- useInterval / useTimeout:定时任务(自动清理)
|
|||
|
|
```tsx
|
|||
|
|
useInterval(() => refresh(), 30000);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- useMemoizedFn:稳定函数引用,避免子组件无谓重渲染/解绑
|
|||
|
|
```tsx
|
|||
|
|
const onNodeClick = useMemoizedFn((id: string) => openDetail(id));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- useLatest:防陈旧闭包,异步/周期函数读取最新数据
|
|||
|
|
```tsx
|
|||
|
|
const latestState = useLatest(state);
|
|||
|
|
useInterval(() => { doSomething(latestState.current); }, 1000);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- useSafeState / useUnmount:卸载安全,避免内存泄漏或报错
|
|||
|
|
```tsx
|
|||
|
|
const [data, setData] = useSafeState<any>(null);
|
|||
|
|
useUnmount(() => cancelRequest());
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## useRequest(核心与进阶)
|
|||
|
|
|
|||
|
|
`useRequest` 是 Ahooks 的异步数据管理核心,内置自动/手动触发、轮询、防抖/节流、屏幕聚焦重新请求、错误重试、loading 延迟、SWR(stale-while-revalidate)缓存等能力。[0][1]
|
|||
|
|
|
|||
|
|
### 基础用法(自动请求)
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import { useRequest } from 'ahooks';
|
|||
|
|
import agentService from '@/services/agent_service';
|
|||
|
|
|
|||
|
|
export default function AgentList() {
|
|||
|
|
const { data, loading, error, refresh } = useRequest(() => agentService.fetchAgentList());
|
|||
|
|
if (loading) return <div>Loading...</div>;
|
|||
|
|
if (error) return <div>Error: {String(error)}</div>;
|
|||
|
|
return <div>{JSON.stringify(data)}</div>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 手动触发与参数管理
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const { run, runAsync, loading, params } = useRequest((kw: string) => agentService.searchAgents({ keywords: kw }), {
|
|||
|
|
manual: true,
|
|||
|
|
onSuccess: (data, [kw]) => console.log('searched', kw, data),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 触发
|
|||
|
|
run('rag');
|
|||
|
|
// 或 await runAsync('rag').catch(console.error);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 生命周期回调
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
useRequest(api, {
|
|||
|
|
onBefore: (p) => console.log('before', p),
|
|||
|
|
onSuccess: (d) => console.log('success', d),
|
|||
|
|
onError: (e) => console.error('error', e),
|
|||
|
|
onFinally: (p, d, e) => console.log('finally'),
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 刷新上一次请求(refresh)与数据变更(mutate)
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const { run, refresh, mutate, data } = useRequest(() => api.getUser(1), { manual: true });
|
|||
|
|
|
|||
|
|
// 修改页面数据,不等待接口返回
|
|||
|
|
mutate((old) => ({ ...old, name: 'New Name' }));
|
|||
|
|
// 使用上一次参数重新发起请求
|
|||
|
|
refresh();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 取消响应(cancel)与竞态取消
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const { run, cancel } = useRequest(api.longTask, { manual: true });
|
|||
|
|
run();
|
|||
|
|
// 某些场景需要忽略本次 promise 的响应
|
|||
|
|
cancel();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 轮询与停止条件
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const { data, cancel } = useRequest(() => agentService.getJobStatus(), {
|
|||
|
|
pollingInterval: 5000,
|
|||
|
|
pollingWhenHidden: false,
|
|||
|
|
onSuccess: (res) => { if (res.done) cancel(); },
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 防抖 / 节流 / 重试 / 延迟 / 焦点刷新
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
useRequest(searchService.query, {
|
|||
|
|
manual: true,
|
|||
|
|
debounceWait: 300,
|
|||
|
|
throttleWait: 1000,
|
|||
|
|
retryCount: 2,
|
|||
|
|
loadingDelay: 200,
|
|||
|
|
refreshOnWindowFocus: true,
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 缓存(cacheKey)与过期时间(staleTime)
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const { data } = useRequest(() => agentService.fetchAgentList(), {
|
|||
|
|
cacheKey: 'agent:list',
|
|||
|
|
staleTime: 60_000, // 1分钟内复用缓存(SWR 策略)
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 条件就绪(ready)与依赖刷新(refreshDeps)
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
useRequest(() => api.getById(id), {
|
|||
|
|
ready: !!id,
|
|||
|
|
refreshDeps: [id],
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 默认参数(defaultParams)
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const { data } = useRequest((id: number) => api.getById(id), {
|
|||
|
|
defaultParams: [1],
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 常见场景模板
|
|||
|
|
|
|||
|
|
### 搜索输入防抖
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const { run: search } = useRequest((kw: string) => api.search({ keywords: kw }), { manual: true });
|
|||
|
|
const { run: onChange, cancel } = useDebounceFn((kw: string) => search(kw), { wait: 300 });
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 任务状态轮询,完成后停止
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const { data, cancel } = useRequest(() => api.getJobStatus(jobId), {
|
|||
|
|
pollingInterval: 5000,
|
|||
|
|
pollingWhenHidden: false,
|
|||
|
|
onSuccess: (res) => { if (res.done) cancel(); },
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 分页列表(前端分页)
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const { data, loading } = useRequest(() => api.list({ page, pageSize, keywords }), {
|
|||
|
|
refreshDeps: [page, pageSize, keywords],
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 依赖请求(根据上游结果触发下游)
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const userReq = useRequest(() => api.getUser(userId), { ready: !!userId });
|
|||
|
|
const postsReq = useRequest(() => api.getPosts(userReq.data?.id), {
|
|||
|
|
ready: !!userReq.data?.id,
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 自动保存(组件生命周期安全)
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
useInterval(async () => {
|
|||
|
|
if (!graphValid) return;
|
|||
|
|
await api.setAgentDSL(payload);
|
|||
|
|
}, 30_000);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 最佳实践与常见坑位
|
|||
|
|
|
|||
|
|
- 顶层调用 Hooks,避免条件语句中调用,保持顺序一致。
|
|||
|
|
- 依赖项变动可能导致频繁触发;`useMemoizedFn` 可保持函数引用稳定。
|
|||
|
|
- 轮询需显式停止;结合 `cancel()` 与 `pollingWhenHidden=false`。
|
|||
|
|
- 缓存需设置合理 `staleTime`,避免过时数据;SWR 适合只读列表数据。
|
|||
|
|
- 组件卸载时自动忽略响应,避免卸载后 setState。
|
|||
|
|
- SSR 环境下注意 `window/document` 可用性,必要时使用 `useIsomorphicLayoutEffect`。
|
|||
|
|
|
|||
|
|
## 与本项目的结合建议
|
|||
|
|
|
|||
|
|
- 知识库列表搜索:将 `setTimeout` 防抖替换为 `useDebounceFn`,代码更简洁且可取消。
|
|||
|
|
- 任务状态轮询:用 `useRequest` 的 `pollingInterval + cancel()` 替代手写 `setInterval`。
|
|||
|
|
- 自动保存:使用 `useInterval`,确保随组件生命周期清理(我们已修复泄漏问题)。
|
|||
|
|
- 事件绑定:用 `useEventListener` 管理 `window`/`document` 事件,自动清理。
|
|||
|
|
- 页面标题:用 `useTitle` 或统一的 `usePageTitle` 封装。
|
|||
|
|
|
|||
|
|
## 速查表(精选)
|
|||
|
|
|
|||
|
|
- 状态:`useBoolean`、`useToggle`、`useSetState`、`useControllableValue`
|
|||
|
|
- 请求:`useRequest`
|
|||
|
|
- DOM/事件:`useEventListener`、`useInViewport`、`useClickAway`
|
|||
|
|
- 定时:`useInterval`、`useTimeout`
|
|||
|
|
- 存储:`useLocalStorageState`、`useSessionStorageState`
|
|||
|
|
- 性能/安全:`useDebounceFn`、`useThrottleFn`、`useMemoizedFn`、`useLatest`、`useSafeState`
|
|||
|
|
|
|||
|
|
## 参考资料
|
|||
|
|
|
|||
|
|
- 官方文档(useRequest 快速上手)[0] https://ahooks.js.org/zh-CN/hooks/use-request/index
|
|||
|
|
- 官方文档(useRequest 基础用法)[1] https://ahooks.js.org/zh-CN/hooks/use-request/basic
|
|||
|
|
- 项目仓库:https://github.com/alibaba/hooks
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
实战建议:如果只需要“更新 web 标题”,直接在页面组件里使用 `useTitle`;若需统一标题规范或根据路由动态拼接,封装一个 `usePageTitle` 更可维护。`useRequest` 作为核心请求管理,优先在网络交互中使用它的自动/手动、轮询、缓存与重试能力,替代手写的定时器与状态机。
|