DEV Community

Scott Hu
Scott Hu

Posted on

I replaced TanStack Query with alova and cut my code by 70%

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 with flatMap.
  • Forms? useMutation handles 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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 */ })
)
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)