How to use debounce with useQuery in React Query?
Asked Answered
S

5

25

I am using React Query to fetch data from an API in a React app. I want to implement debounce for better performance, but I'm having trouble getting it to work with useQuery. When I try to wrap my API call in a debounced function, I get an error saying "query function must return a defined value".

Here is the code I am currently using:

    async function fetchProducts() {
        const response = await axios.get(`/api/products?category_id=${category_id}&${searchParams.toString()}&page=${page}`);
        return response.data;
    }

    const debouncedFetchProducts = React.useMemo(
        () => _.debounce(fetchProducts, 500),
        [fetchProducts]
    );

    // The following queries will execute in parallel
    const categoryQuery = useQuery({ queryKey: ['category'], queryFn: fetchCategory, keepPreviousData: true });
    const productsQuery = useQuery({ queryKey: ['products', category_id, sortPrice, minPrice, maxPrice, page, categoryFilters], queryFn: debouncedFetchProducts, keepPreviousData: true, staleTime: 1000 });

When I run this, I get an error saying "query function must return a defined value". I believe this is because the debounced function returns a promise, but useQuery expects an actual value.

I have also tried using useAsync, but I would like to use useQuery because it has built-in caching.

Can someone help me figure out how to implement debounce with useQuery in React Query?

Thank you in advance for your help!

Snowinsummer answered 17/4, 2023 at 0:35 Comment(0)
O
47

You can utilize the useDebounce hook to trigger a queryKey update in react-query instead of using the debounce function from the Underscore library.

For example:

const [searchParams, setSearchParams] = useDebounce([category_id, sortPrice, minPrice, maxPrice, page, categoryFilters], 1000)
const productsQuery = useQuery({ queryKey: ['products', ...searchParams], queryFn: fetchProducts, keepPreviousData: true, staleTime: 1000 });

useDebounce is applied to the searchParams array, which includes variables like category_id, sortPrice, minPrice, maxPrice, page, and categoryFilters.

The debounce delay is set to 1000 milliseconds (1 second). The productsQuery then uses the debounced search parameters in its query key, ensuring that the fetchProducts function is only called when the debounced search parameters change.

You can find a working useDebounce example in this codesandbox example

Overeager answered 17/4, 2023 at 4:22 Comment(3)
I'm glad the solution worked for you.Overeager
For people using the latest version as of July 2024, replace keepPreviousData: true with placeholderData: (prev) => prevHalftrack
You can also set placeholderData: keepPreviousData as discussed here: tanstack.com/query/latest/docs/framework/react/guides/…Halftrack
A
2

You can use the React build-in hook useDeferredValue

Showing stale content while fresh content is loading Call useDeferredValue at the top level of your component to defer updating some part of your UI.

Alleris answered 18/11, 2023 at 6:11 Comment(4)
I'm not quite sure that useDefferedValue would help with react-query. See the note at the bottom of your link: "If the work you’re optimizing doesn’t happen during rendering, debouncing and throttling are still useful. For example, they can let you fire fewer network requests.". useDeferredValue is better suited to optimizing rendering, not reducing network requests.Accordingly
React-query uses a built-in mechanism to cache data, so it will not re-fetch for the same API with the same params unless the data is stale. For example, if you use a search input that requests data on every 'onChange' event, the application will re-render after every key, with different API request params. That will cause a new API quest for every re-render even using react-query. Using useDeferredValue will reduce the number of re-renders of our applicationAlleris
You misinterpreted the docs. Yes react-query caches result, but as you can see, react-query is still called after each keystroke with useDeferredValue: see console in sandbox here. useDeferredValue still re-renders in the background. The only diff is that it might not commit intermediate changes if the client overloaded.Accordingly
"Note that there is still a network request per each keystroke. What’s being deferred here is displaying results (until they’re ready), not the network requests themselves." - react.devCunha
L
2

I used the AbortSignal parameter with a sleep() on a useMutation to replicate debounce behaviour ootb. It should be the same for useQuery.

Explanation:

  • whenever a useQuery is re-triggered, react-query cancels the previous inflight request (based on queryKey).
  • the useQuery implementation below, waits half a second, and then checks whether the signal (i.e. this request attempt) has been aborted.
  • if not aborted, proceed!

One more thing...

  • pass the signal through to the axios request - that way the request (if somehow inflight) can be cancelled. Useful for big payloads!

e.g.

const sleep = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms))

const categoryQuery = useQuery({
    queryKey: ['category'],
    queryFn: async ({ signal }) => {
        await sleep(500)
        if (!signal?.aborted) {
            const response = await axios.get(`/api/products?category_id=${category_id}&${searchParams.toString()}&page=${page}`,
                { signal });
            return response.data;
        }
    },
    keepPreviousData: true
});

More information:

Related:

Lidalidah answered 22/5, 2024 at 13:8 Comment(0)
H
1

Following up on Cody Chang's response, as of 2024, TanStack query updated keepPreviousData to placeholderData. Below is an example I wrote:

import { useDebounce } from 'use-debounce';
import { keepPreviousData, useQuery } from '@tanstack/react-query';

const DEBOUNCE_MS = 300;

function QuickSwitcher() {
    const [open, setOpen] = useState(false);
    const [query, setQuery] = useState('');
    const [debouncedQuery] = useDebounce(query, DEBOUNCE_MS);
    const queryResult = useQuery({
        queryKey: ['quick-switcher', debouncedQuery],
        queryFn: async () => {
            if (!debouncedQuery) return [];
            const rows = await actions.pages.getPagesByFts({
                query: debouncedQuery,
            });
            return rows;
        },
        staleTime: DEBOUNCE_MS,
        placeholderData: keepPreviousData,
    });
    return (
        <CommandDialog open={open} onOpenChange={setOpen}>
            <Input
                placeholder="Type a command or search..."
                value={query}
                onChange={(e) => {
                    const newValue = e.target.value;
                    setQuery(newValue);
                }}
            />
        ...
        </CommandDialog>
    )
}
Halftrack answered 29/7, 2024 at 6:49 Comment(0)
M
0

In order to make lodash debounce to work with react-query, you’ll have to enable leading run of the function. Trailing run is required tanke sure you get the latest result.

debounce(fetchProducts, 500, {leading: true, trailing: true})

Source: https://lodash.com/docs/#debounce

Minim answered 12/5, 2024 at 11:29 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.