I Stopped Using useQuery for useRequest — Here’s Why
Over the past year, I’ve slowly moved most of my React data-fetching logic from useQuery in TanStack Query to useRequest from ahooks.
For a long time, useQuery was my default choice for data fetching in React.
It felt like the obvious solution.
But after working on a few real-world projects, I started to feel that things could be simpler.
So I decided to try something different — useRequest.
And surprisingly… I haven’t looked back.
Main Reason: Simpler and Easier to Understand
When working with TanStack Query, I often found myself stopping to think: what’s actually the difference between useQuery and useMutation?
In general, useQuery is used for fetching and caching server data, while useMutation is meant for updating data and triggering requests manually.
The distinction makes sense — but in day-to-day development, it can feel more complicated than it needs to be.
Let’s take a look at one of the most common use cases that almost every developer runs into.
// get data
const { data } = useQuery({
queryKey: ['get-data'],
queryFn: () =>
fetch('/api/user')
.then((res) => res.json())
})
// update data
const { data, mutate } = useMutation({
mutationFn: () => fetch('/api/user', {
method: 'POST',
body: JSON.stringify({ username: "edwin" })
})
})
mutate()
This is a pattern developers end up writing over and over again — just with different APIs and slightly different configuration names each time.
But what if you want to fetch GET /api/user on demand?
Suddenly, you’re forced to switch from useQuery to useMutation.
And that’s… kind of annoying
Now, let’s look at the same example using useRequest.
// get data
const { data } = useRequest(
() => fetch('/api/user').then((res) => res.json())
)
// update data
const { data, run } = useRequest(
() => fetch('/api/user', {
method: 'POST',
body: JSON.stringify({ username: "edwin" })
}),
{
manual: true
}
)
run()
It’s noticeably simpler — and it actually makes more sense.
The only difference is a single configuration option: manual, which controls whether the request runs automatically or only when you trigger it yourself.
Both modes share the exact same API and developer experience — just with a different configuration flag.
Success and Error
When you need to implement logic in onSuccess and onError callbacks, the difference becomes even more noticeable — especially with TanStack Query.
useQuery
const { data, isSuccess, isError } = useQuery({...})
useEffect(() => {
if (isSuccess) {
// do something on success
}
}, [isSuccess, data])
useEffect(() => {
if (isError) {
// do something on error
}
}, [isError])
useMutation
// update data
const { data, mutate } = useMutation({
mutationFn: () => {...},
onSuccess: () => {
// do something on Success
},
onError: () => {
// do something on Error
}
})
See, what should be a simple pattern can quickly start to feel inconsistent and harder to reason about. 🤯
With useRequest, the developer experience stays consistent. ✅
// work for both get or update
const { ... } = useRequest(
() => {...},
{
onSuccess: () => {
// do something on Success
},
onError: () => {
// do something on Error
}
}
)
Bonus Debounce and Throttle
If you need to prevent API spamming — especially when users trigger requests repeatedly by clicking buttons — this feature can save you a lot of time.
Instead of implementing your own debounce or throttle logic, you can simply define the timing you want.
// debounce
const { data, run } = useRequest(getUsername, {
debounceWait: 300,
manual: true
})
// throttle
const { data, run } = useRequest(getUsername, {
throttleWait: 300,
manual: true
})
This is a feature I consider essential — and yet it’s still missing in TanStack Query.
Conclusion
TanStack Query is powerful, but many real-world apps don’t actually need all of that power.
If your use case looks similar to mine, you might not need useQuery at all.
useRequest offers a lighter, simpler, and more developer-friendly alternative.
Hopefully, this helped you rethink how you handle data fetching in your React apps.