query: Cannot debounce API calls
I am trying to use this for a search component, like:
const { status, data, error, isFetching } = useQuery(
searchQuery && ['results', searchQuery],
getSearchAsync,
);
I would like the call to getSearchAsync to be debounced while the user is typing the query. Tried adding lodash.debounce like:
const { status, data, error, isFetching } = useQuery(
searchQuery && ['results', searchQuery],
debounce(getSearchAsync, 3000),
);
But since this is a function component it is not working as expected. The getSearchAsync method is only called multple times after the timeout expires, which I expect to be once with the latest value (searchQuery).
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 3
- Comments: 35 (3 by maintainers)
I’d like to see this, since react-query is serializing the key it’s doing all the heavy lifting there, and could then debounce the result of the key (which devs have no hook into that result).
Can this be reopened?
@denisborovikov thanks…that works nicely! Still…wouldn’t it be nice to have this as an option in react-query?
@tannerlinsley could this be reconsidered? Debouncing based on the serialized query key would be an elegant solution, and in my view a reasonable responsibility for
react-query. I’m interested to try my hand in making a PR if it has any chance of getting merged.You may try to debounce
searchQuerychange instead, something like that.For anyone who is interested in debouncing a mutation, I hacked together this hook. It returns a mutation with the
debouncedMutatefunction which acceptsdebounceMsas an option. Apologies for no typescript.Example use:
I’m using this hook in a chat app to check if a user inputted channel name already exists.
Edit: As pointed out by @jjorissen52,
timershould be a ref so that it is not reinitialized on each render. I have updateduseDebouncedMutationaccordingly 🤙Another issue seems to arise when dealing with debouncers. Debounced
Promiseobjects get stuck in a"loading"state within React Query and stay insideMutationCacheforever.I’m fairly new to React Query but I haven’t figured out how to serialize mutations to the same entry. It’s necessary in the case of updating an input and avoid race conditions of two update queries. Am I wrong in thinking that this should be handled by React Query? Whatever I search to that end resolves here but even debouncing has this race condition, just less likely to happen. Is there a configuration for serializing updates? How about buffering updates while there is one in progress?
Wow…there is no debounce package that does this? Or even better…can’t react-query have an option for this?
Would really be cool to have a debounceMs option in useMutation, with default value = 0. It would be a really good ready to use functionality. +1
@AndreSilva1993 I think the solution could be simpler. Unless I’m mistaken, if you just want to know if something has been selected that hasn’t started fetching yet because it’s debounced, all you’d need is to compare selections:
now you can check for
isTransitioningto show a loading spinner instead of data for item A (debouncedSelected), because you’ve selected itemB/C/D (selected) instead.https://github.com/tannerlinsley/react-table/blob/master/src/publicUtils.js
This implementation does not always debounce. Though the
debouncedMutatecloses over the context containingtimer, the blocklet timerwill execute every time the component re-renders, effectively callingclearTimeout(null)and failing to clear the timeout. Here is a modified version that will debounce even between parent component renders:Saving the
Timeoutwith a ref means it will be available between renders.I’m here because I need a debounced mutate in a way it doesn’t seem anyone else does. I’m using a mutation to save changes to a prose document. Since the user can be typing very fast, I only update once per second.
I’m looking for a debounced mutation that interacts well with optimistic queries. So the version above at https://github.com/TanStack/query/issues/293#issuecomment-1238760706 doesn’t suffice.
I’d like to be able to do:
And have
onMutatecalled immediately so that the optimistic value is shown to the user without a delay.The snippet linked wouldn’t show the optimistic value until 1s later.
This is also a non-trivial feature to implement because if you implement it within your query function, the mutation calls can pile up - do you really want that? There’s no good response to the intermediate queries that never actually ran. It’s not an error, but it’s also not success.
And if you implement it outside of useMutation, you’ve got to reimplement/mess with your own onMutate and onSettled for optimistic queries.
Here’s roughly what I have now. Call it like:
Implemented like (not exactly - paraphrased, hopefully still works after the paraphrasing):
where debounceP is
And
debounceis similar with out the Promise stuff:The big gotcha to this implementation is that intermediate mutations are thrown away! So you have to be sure that the latest mutation always includes previous ones as well (which is true in my case, based on the nature of text editing).
Hope this helps someone else!
UPDATE: And of course I’m already noticing things like the “old” context is now wrong in my onMutate. So onError, the value will be reset only back to the last update, not the last successful update. Definitely wish this were part of the library 😃
Pretty much, yes. You would have to debounce the whole options:
by “only” debouncing the key, we would put a refetchInterval on the input with value 0 if we transition from 0 to 1 for example. We would have to debounce everything, which is literally what debouncing the input in userland is.
Yeah that’s true, it’s a general tradeoff when debouncing the queryKey. But it’s the same issue as written above - I don’t think we can easily just debounce the key or the queryFn, we need to debounce everything.
Don’t want to sunshine the tradeoff, but I think it usually doesn’t matter much to have a bunch of millis delay in rendering, and it might even be a good thing. If I’m typing
hello worldand I have results in the cache forhelloandhello wand hellowo, I would get 3 intermediate renders with data I’m not really interested at. You might also need to go into concurrent features then to make those renders smooth.absolutely, PRs do the docs are always welcome ❤️
For me, if I type very fast I cannot see the input. Only after I finish typing I see the text. After that, all works fine. Here is a my code:
Ok, I was definitely over-complicating things. I think your example more than suffices for what I’m trying to accomplish.
@vincerubinetti I get that this a feature requested by many, but there are always tradeoffs:
react-query does not have the smallest bundle size and not the most simplistic api surface. The stance is that features that can be implemented in user-land with minimal effort will not make it into the library. Interestingly, from the things you’ve mentioned: react-query doesn’t do anything for “pagination”. The pagination uses a feature that was meant to mimic suspense/transitions (keepPreviousData). Unless you’re thinking of inifinte queries, which is something that isn’t easily doable in user-land with raw useQuery.
Now for debouncing, the problems are:
lodash/debounceadds another kilobyte, about 10% of the total lib size. I know bundle-size is an argument that I personally dislike, too, but it’s something that we sadly have to keep in mind.now, the queryKey would change with every input, but we wouldn’t want to start observing that key (switching to that queryObserver) before debounce time. That also means that all other options changes should not be applied, because they “belong to” the new selection key. So I think this opens up a can of worms for no real gain. Lots of complexity and edge cases within the library. Basically, a key change doesn’t / shouldn’t do anything if debounce is specified. That is hard to justify and explain.
Just want to say… I know it’s not a deal-breaker to use debounced versions of query key variables, but you could also say that about implementing caching, or pagination, or one of the seemingly many many other use/edge cases that React Query covers. It’s kind of weird that RQ chooses to cover all that other complex and niche stuff, but not this very common (as you can tell by all the 👍s) need. This issue shouldn’t be dismissed.
You need to use a debounce that supports promises. It needs to return a promise immediately, then debounce off of that promise.
If you are here looking for some Vue answer, I used the watchDebounced from vueUse and it looks something like this:
Hope it helps!
I’ve been using use-debounce since I commented here 2 weeks ago and it works great (lodash one didn’t seem too good, maybe as it’s not a proper tool for react?).
Still, I would like it if react-query had plugins to be optionally installed, so they wouldn’t add additional weight for common users but only for those who opt-in for that. I don’t know much about this topic so I can’t go further on it, but this is my conclusion about this.
Edit: Or maybe at least add those solutions to the Docs so they don’t appear hacky to bosses?
@tbntdima If you use
searchTermas your input’s value prop you should see as you type. You probably use debouncedSearchTerm instead and that’s why the input is denounced. Debounced value should be used for the query only.You don’t really need any of React hooks to handle debounce, here’s an example with old version of
axios.@TkDodo Only caveat I’ve now realised with the solution you provided (should be a question of preference and will definitely ponder on this) is that even if react-query has already fetched the results for a specific item, I’ll always display the loading spinner for the amount of time I’m debouncing items whereas, if the fetch is the one being debounced but the queryKey changes immediately, I get the cached results straight away.
So my question is, if we decide to go with the custom hook do you see any evident issues or pitfalls that I might run into? And thanks for your time by the way.
Found this thread since I have the same issue and debouncing the change does not work for me. Let’s imagine the scenario in which I debounce the change for 250ms.
During the selection of item B, C and D I’m still showing the details of item A since I’m debouncing its change. So I wanted to debounce the fetch request since I want to display the loading spinner indicating that the current item is being loaded but not fire the request immediately. Otherwise the user does not have an immediate feedback that something is actually happening.
I tested and I reckon this works but wanted to know your thoughts!
The
cancelQueriesshould avoid the infiniteloadingstate due to the Promises never being either resolved or rejected.FYI This link doesn’t work anymore
Thanks @mrlubos! I actually did something very interesting. I used
initialDatato store the debounced function (simple debounce with lodash) inside the data. I then make thequeryFncall the method inside data. I then store the returned data in a nested key in data. Funky, but works very smoothly, it respects the react-query useQuery cache.I probably found something very similar to
react-querybut withdebouncebuilt in.https://ahooks.js.org/hooks/use-request/debounce
@denisborovikov the problem is a bit different. When
searchTermchanges, the component was re-rendering anyway or at least tried to. And because of this, I was getting lags. My eventual solution was to move the denounce login into the upper component 🤔