我弃用了 TanStack Query,换成了 alova,代码量砍掉 70%
一个前端老鸟的请求库迁移实录
故事的开始
半年前接手了一个中台项目,需求列表长得像购物小票:分页列表、多步骤表单、实时通知(SSE)、文件上传、搜索自动补全......
刚开始我理所当然地选择了 TanStack Query(以前叫 React Query),毕竟是 React 请求库的"顶流"。但项目越做越不对劲------为了应付不同的请求场景,我在 TanStack Query 外面套了一层又一层的封装:
- 分页?自己写
useInfiniteQuery+ 手动管理 page state + 拼数据 - 表单?
useMutation只管提交,表单状态、草稿保存、重置全得自己来 - SSE?TanStack Query 不支持,我去找了
@microsoft/fetch-event-source - 服务端重试?TanStack Query 只管客户端......
项目结束后我数了数,光是请求相关的代码就有 2000+ 行。
直到有一天,一个同事丢过来一个链接:alova。
第一次震撼:分页从 50 行到 3 行
先来看最直观的对比。
Before: TanStack Query 实现分页
tsx
import { useInfiniteQuery } from '@tanstack/react-query'
const PAGE_SIZE = 10
function TodoList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isError,
error,
} = useInfiniteQuery({
queryKey: ['todos'],
queryFn: async ({ pageParam = 1 }) => {
const res = await fetch(`/api/todos?page=${pageParam}&pageSize=${PAGE_SIZE}`)
return res.json()
},
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? allPages.length + 1 : undefined
},
})
if (isLoading) return <Loading />
if (isError) return <Error message={error.message} />
const todos = data?.pages.flatMap(page => page.items) ?? []
return (
<div>
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? '加载中...' : '加载更多'}
</button>
)}
</div>
)
}
这已经是最简写法了------20 行组件 + 服务端还得配合 hasMore 字段。如果再加上 loading、error 的状态枚举、空数据处理、刷新控制、页码展示......轻松突破 50 行。
After: alova usePagination
tsx
import { usePagination } from 'alova/client'
function TodoList() {
const {
loading, data, page,
pageSize, total,
nextPage, prevPage,
} = usePagination(
(page) => alova.Get('/api/todos', { params: { page, pageSize: 10 } }),
{ total: res => res.total }
)
if (loading) return <Loading />
return (
<div>
{data.map(todo => <TodoItem key={todo.id} todo={todo} />)}
<Pagination
page={page}
total={total}
onNext={nextPage}
onPrev={prevPage}
/>
</div>
)
}
3 行核心代码 。page、pageSize、total、翻页、加载状态全部内置。不需要 useInfiniteQuery,不需要手动拼 pages.flatMap,不需要手写 getNextPageParam 逻辑。
这可能是我写前端以来最爽的一次分页实现。
5 个场景,逐个对比
场景 1:表单提交 + 草稿持久化
TanStack Query 的做法:
tsx
const mutation = useMutation({
mutationFn: (data) => api.submitForm(data),
})
// 表单状态靠自己
const [formData, setFormData] = useState({})
const [draft, setDraft] = useState(() => {
return JSON.parse(localStorage.getItem('formDraft') || '{}')
})
useEffect(() => {
localStorage.setItem('formDraft', JSON.stringify(formData))
}, [formData])
// 提交
const handleSubmit = () => mutation.mutate(formData)
alova 的做法:
tsx
const {
form, loading, sendForm,
updateForm, reset,
} = useForm((formData) => alova.Post('/api/submit', formData), {
initialForm: {},
store: true, // 自动草稿持久化
})
// 提交
const handleSubmit = () => sendForm()
store: true 一键开启草稿本地持久化,页面刷新表单数据还在。不用写 useEffect、不用手动操作 localStorage、不用管理一个额外的 mutation。
场景 2:实时通知(SSE)
TanStack Query:
SSE 不在 TanStack Query 的职责范围内。我需要额外安装 @microsoft/fetch-event-source,然后自己管理连接状态、重连、消息解析:
tsx
// 第三方库 + 手动管理
import { EventSource } from '@microsoft/fetch-event-source'
useEffect(() => {
const es = new EventSource('/api/notifications')
es.onmessage = (event) => {
setNotifications(prev => [...prev, JSON.parse(event.data)])
}
es.onerror = () => { /* 手动重连 */ }
return () => es.close()
}, [])
alova 的做法:
tsx
const { data, readyState } = useSSE(
alova.Get('/api/notifications', {
// SSE 配置
})
)
readyState 自动反映连接状态(CONNECTING / OPEN / CLOSED),data 是一个响应式流,拿来就用。
场景 3:自动轮询 + 焦点刷新 + 节流
TanStack Query:
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchInterval: 3000, // 每 3 秒轮询
refetchOnWindowFocus: true,
})
alova 的做法:
tsx
useAutoRequest(alova.Get('/api/todos'), {
throttle: 3000, // 节流 3 秒
enablePoll: true, // 启用轮询
enableFocus: true, // 焦点刷新
})
本身差不多,但 alova 的 throttle 参数可以同时控制轮询和焦点刷新的最小间隔,不会出现焦点来回切换疯狂刷新的问题。
场景 4:请求失败自动重试(服务端)
TanStack Query:
tsx
// 只在客户端有效
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retry: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
})
alova 服务端重试:
tsx
import { retry } from 'alova/server'
// 在 Node.js / Bun / Deno 中都可以用
const result = await retry(
alovaInstance.Post('/api/order', orderData),
{ retry: 3, backoff: { start: 1000, multiplier: 2 } }
).send()
而且 alova 还支持分布式速率限制:
tsx
import { RateLimiter } from 'alova/server'
const limiter = new RateLimiter({
points: 10, // 10 个请求
duration: 1, // 每秒
})
// 配合 Redis 跨进程共享限制状态
const result = await limiter.limit(
alovaInstance.Get('/api/external')
).send()
这两个能力 TanStack Query 根本覆盖不了------它是纯客户端库,服务端场景力不从心。
场景 5:跨框架复用
TanStack Query 在 React 项目里很香,但你换个 Vue 项目呢?再学一套 @tanstack/vue-query?然后小程序呢?Node.js 后端呢?
alova 一套 API 走天下:
tsx
// React 项目
const { data } = useRequest(alova.Get('/api/todos'))
// Vue 项目 --- 只需换 statesHook
const { data } = useRequest(alova.Get('/api/todos'))
// Svelte 项目 --- 一样
const { data } = useRequest(alova.Get('/api/todos'))
// 小程序 (UniApp / Taro) --- 一样
const { data } = useRequest(alova.Get('/api/todos'))
// Node.js 后端 --- 直接 await
const data = await alova.Get('/api/todos')
import 不同,API 相同。 一套心智模型覆盖所有场景。
数据说话:代码量对比
| 场景 | TanStack Query | alova | 减少 |
|---|---|---|---|
| 分页列表 | ~50 行 | 3 行 | 94% |
| 表单提交 + 草稿 | ~35 行 | 3 行 | 91% |
| SSE 实时通知 | ~30 行(第三方库) | 1 行 | 97% |
| 自动轮询 | ~10 行 | 2 行 | 80% |
| 失败重试 | ~8 行 | 1 行 | 87% |
| 总计(5 个场景) | ~133 行 | ~10 行 | 92.5% |
当然这不是说 TanStack Query 不好------它是一个优秀的库,在「缓存 + 状态管理」这个点上做得非常好。但它定位的是 "tool",而不是 "strategy"。每一个业务场景你都需要自己去组合它的基础能力。
alova 的答案是:把这些常见场景直接封装成策略,开箱即用。
就像你用 React 不需要自己实现虚拟 DOM diff,用 alova 你也不需要自己实现分页逻辑。
什么时候选谁?
- 简单的 CRUD + 缓存场景:TanStack Query 完全够用,没有任何问题
- 复杂的中后台 / BFF 项目:涉及的请求模式多,alova 的策略思维帮你节约大量时间
- 跨框架 / 跨端项目:alova 的框架无关性是硬优势
- Node.js 服务端需要请求控制:重试、限流、分布式锁,alova 是唯一选择
最后
说实话,作为一个开源作者,我深知做一个好库有多难,而让好库被人知道更难。alova 的 v3 版本已经非常成熟了,20+ 请求策略、客户端+服务端全覆盖、框架无关,这些能力不应该只被 4k 人看到。
如果你看完了觉得有点意思:
🌐 官网: alova.js.org
也欢迎留言讨论------你觉得请求库的下一个进化方向是什么?