⚠️ 【待 review】
Why TanStack Query is overrated (and what to use instead)
I've been a TanStack Query user for two years. I wrote about it, recommended it to my team, and genuinely thought it was the final form of frontend data fetching.
Then I built a real product — a full-blown admin dashboard with forms, paginated lists, file uploads, polling, and cross-component communication.
That's when I realized: TanStack Query gives you a glorified fetcher and leaves all the complexity to you.
It's not that TanStack Query is bad. It's excellent at what it does: query caching, request deduplication, and background refetching. But in real-world application development, "fetching data" is only a fraction of what you actually need.
Here are 5 scenarios where I found alova drastically out-performs TanStack Query.
1. Form Submission
TanStack Query
const { mutateAsync } = useMutation({
mutationFn: (data) => axios.post('/api/form', data),
})
const { register, handleSubmit, reset, watch } = useForm({
defaultValues: loadFromDraft()
})
useEffect(() => {
saveDraft(watch())
}, [watch()])
const onSubmit = async (data) => {
await mutateAsync(data)
reset()
clearDraft()
}
You need React Hook Form (or Formik), manual draft persistence, manual reset after submit — easily 30+ lines for what should be a one-liner.
alova
const {
loading, submit, form, reset
} = useForm(
(formData) => alova.Post('/api/form', formData),
{
store: true, // auto draft persistence
resetAfterSubmiting: true, // auto reset after submit
}
)
3 lines. Draft persistence survives page refresh. Multi-step form state is shared automatically. No useEffect, no localStorage calls.
The philosophy difference: TanStack Query gives you a primitive and says "figure it out." alova gives you a complete form abstraction and says "just use it."
2. Pagination / Infinite Scroll
TanStack Query
const [page, setPage] = useState(1)
const { data, isPreviousData } = useQuery({
queryKey: ['list', page],
queryFn: () => fetch(`/api/list?page=${page}`),
keepPreviousData: true,
})
// Manual prefetch
const queryClient = useQueryClient()
const prefetchNext = () => {
queryClient.prefetchQuery(['list', page + 1], ...)
}
useEffect(() => {
if (data?.length < pageSize) return
prefetchNext()
}, [page, data])
This is just page management. Once you add edit-then-refresh or delete-then-remove, the boilerplate doubles.
alova
const {
data, loading, page, pageSize,
handlePrevPage, handleNextPage, reload
} = usePagination(
(page, pageSize) => alova.Get('/api/list', {
params: { page, pageSize }
}),
{
preloadNextPage: true, // auto prefetch next page
total: res => res.total,
}
)
usePagination manages the entire pagination lifecycle — page state, prefetching, list mutation — in one hook. Deleting an item? It's automatically removed from the list. Editing one? Automatically updated.
3. Smart Polling + Focus Refetch
TanStack Query
const { data } = useQuery({
queryKey: ['notification'],
queryFn: () => fetch('/api/notification'),
refetchInterval: 5000,
refetchOnWindowFocus: true,
})
Looks fine until you need dynamic control:
const [enabled, setEnabled] = useState(false)
const { data } = useQuery({
queryKey: ['notification'],
queryFn: () => fetch('/api/notification'),
refetchInterval: enabled ? 5000 : false,
refetchOnWindowFocus: enabled,
})
alova
const { loading, data, stop, resume } = useAutoRequest(
() => alova.Get('/api/notification'),
{
enablePoll: true,
pollInterval: 5000,
enableThrottle: true, // stop when tab is hidden
enableVisibility: true, // resume when tab regains focus
}
)
// Full control
stop() // pause
resume() // restart
Polling, focus refetch, throttling, and visibility control — all in one hook. Call stop() and resume() whenever you need.
4. Cross-Component Communication
TanStack Query
// Component A
const mutation = useMutation({
mutationFn: (data) => axios.post('/api/item', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['list'] })
},
})
As your app grows, invalidateQueries calls scatter across your codebase. Tracking queryKey dependencies becomes a manual mental exercise.
alova
// Declarative cache invalidation
const updateTodo = (id, data) =>
alova.Put(`/api/todo/${id}`, data, {
hitSource: 'todoList', // auto-invalidate todoList cache
})
Anyone reading the code immediately sees: "updating a todo → refreshes the list." No need to hunt down mutation callbacks.
For more complex scenarios, actionDelegationMiddleware lets you call another component's request method from anywhere — no prop drilling, no global store:
// In Component A:
const { send } = useRequest(updateTodo, {
middleware: actionDelegationMiddleware('updateTodo')
})
// In Component B (anywhere in the tree):
accessAction('updateTodo', ({ send }) => { send() })
5. File Upload
TanStack Query — No native support
You'd write your own upload logic with FormData, progress events, concurrency control, and pause/resume. Every. Single. Time.
alova
const {
upload, loading, progress
} = useUploader(
({ file }) => alova.Post('/api/upload', { file }),
{ limit: 3 } // max 3 concurrent uploads
)
// Pause / resume
controller.pause()
controller.resume()
6 lines. Progress tracking, concurrency control, pause/resume — all built in.
The Bottom Line
| Scenario | TanStack Query | alova |
|---|---|---|
| Basic GET + cache | ✅ Works well | ✅ Works well |
| Form + draft + reset | ❌ DIY ~200 lines | ✅ useForm: 3 lines |
| Pagination + prefetch | ❌ DIY | ✅ usePagination |
| Smart polling + throttle | ❌ DIY | ✅ useAutoRequest |
| Cross-component communication | ⚠️ invalidateQueries | ✅ hitSource / actionDelegation |
| File upload + concurrency | ❌ Not supported | ✅ useUploader |
| Bundle size | ~13KB gzipped | ~4KB gzipped |
TanStack Query is an excellent library. For simple data fetching and caching, it's a solid choice.
But if your application deals with forms, paginated lists, file uploads, or any of the "messy real-world" request scenarios, alova's strategy hooks will save you hundreds of lines of code and countless hours of debugging.
Give it a try — you might be surprised how simple data fetching can be.
Top comments (0)