I replaced TanStack Query with alova and cut my code by 70%
A frontend engineer's real-world migration story — 5 scenarios, 133 lines reduced to 10 (92.5% reduction)
Six months ago I took over a mid-office project with a wishlist the size of a CVS receipt: paginated lists, multi-step forms, real-time SSE notifications, file uploads, autocomplete search…
Naturally, I reached for TanStack Query (formerly React Query). It's the de facto standard for React data fetching, right?
But as the project grew, something felt off. To handle different request patterns, I kept wrapping TanStack Query with more and more custom logic:
-
Pagination? Write
useInfiniteQuery+ manually manage page state + flatten pages withflatMap. -
Forms?
useMutationhandles submission only — form state, draft saving, reset, all DIY. -
SSE? TanStack Query doesn't support it. I pulled in
@microsoft/fetch-event-source. - Server-side retry? TanStack Query is client-only.
By the end, I counted 2,000+ lines of request-related code.
Then a colleague dropped a link: alova.
First shock: Pagination went from 50 lines to 3
Before: TanStack Query
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 ? 'Loading...' : 'Load more'}
</button>
)}
</div>
)
}
After: alova usePagination
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 lines of core logic. page, pageSize, total, navigation, loading state — all built in. No useInfiniteQuery, no manual page state, no flatMap.
5 scenarios, side by side
1. Form submission + draft persistence
TanStack Query: You get useMutation for submission. Everything else — form state, localStorage draft, sync logic — is on you.
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: One store: true flag = automatic draft persistence.
const {
form, loading, sendForm,
updateForm, reset,
} = useForm((formData) => alova.Post('/api/submit', formData), {
initialForm: {},
store: true, // auto-draft to localStorage
})
const handleSubmit = () => sendForm()
2. Real-time SSE notifications
TanStack Query doesn't support SSE out of the box. You need a third-party library plus manual connection/reconnection logic.
alova has built-in useSSE:
const { data, readyState } = useSSE(
alova.Get('/api/notifications', { /* SSE config */ })
)
readyState automatically reflects connection status (CONNECTING/OPEN/CLOSED). data is a reactive stream.
3. Auto-polling + focus refetch + throttling
Both libraries support polling and focus refetch. But alova's throttle parameter controls the minimum interval for both polling and focus refetch — no frantic re-fetching when the user keeps tab-switching.
useAutoRequest(alova.Get('/api/todos'), {
throttle: 3000, // 3s throttle
enablePoll: true, // polling on
enableFocus: true, // focus refetch on
})
4. Server-side retry + rate limiting
TanStack Query is client-only. Its retry option only works in the browser.
alova's retry and RateLimiter work in Node.js, Bun, and Deno — and RateLimiter even supports Redis for distributed rate limiting across processes.
import { retry, RateLimiter } from 'alova/server'
// Server-side retry with exponential backoff
const result = await retry(
alovaInstance.Post('/api/order', orderData),
{ retry: 3, backoff: { start: 1000, multiplier: 2 } }
).send()
// Distributed rate limiting
const limiter = new RateLimiter({
points: 10, // 10 requests
duration: 1, // per second
})
const result = await limiter.limit(
alovaInstance.Get('/api/external')
).send()
This is a category of functionality TanStack Query simply cannot provide.
5. Cross-framework consistency
TanStack Query has different APIs per framework (@tanstack/react-query, @tanstack/vue-query, @tanstack/svelte-query).
alova uses the same API everywhere:
// React
const { data } = useRequest(alova.Get('/api/todos'))
// Vue — same API
const { data } = useRequest(alova.Get('/api/todos'))
// Svelte — same API
const { data } = useRequest(alova.Get('/api/todos'))
// Mini-programs (UniApp/Taro) — same API
const { data } = useRequest(alova.Get('/api/todos'))
// Node.js backend — just await
const data = await alova.Get('/api/todos')
Different imports, identical API. One mental model for every environment.
By the numbers
| Scenario | TanStack Query | alova | Reduction |
|---|---|---|---|
| Paginated list | ~50 LOC | 3 LOC | 94% |
| Form + draft | ~35 LOC | 3 LOC | 91% |
| SSE notifications | ~30 LOC (3rd party) | 1 LOC | 97% |
| Auto-polling | ~10 LOC | 2 LOC | 80% |
| Retry (server) | ~8 LOC | 1 LOC | 87% |
| Total (5 scenarios) | ~133 LOC | ~10 LOC | 92.5% |
This isn't to say TanStack Query is bad — it's excellent at what it does (caching + state management). But its philosophy is "here are the tools, you build the solution."
alova takes a different approach: 20+ built-in request strategies for real-world scenarios. Pagination, forms, SSE, polling, retry, rate limiting — they're all ready to use, not ready to build.
When to choose what?
- Simple CRUD + caching: TanStack Query is fine. No issues.
- Complex mid-office / BFF projects: Multiple request patterns → alova's strategy-based approach saves significant time.
- Cross-framework / cross-platform projects: alova's framework-agnostic design is a clear advantage.
- Server-side request control: Retry, rate limiting, distributed locking → alova is the only choice.
⭐ GitHub: alovajs/alova
🌐 Site: alova.js.org
What do you think is the next evolution of request libraries? Drop a comment below.

