Search and pagination
Debounce the value, cache every search, and keep the old page on screen while the next one loads.
From: the multi-vendor e-commerce platform (storefront search and order history). The resort website's admin dashboard tables use the same pagination setup.
Debounced search
Typing "sneakers" without any care sends eight requests, one per letter. Only the last answer matters. Two fixes, together:
Debounce the value, not the request
const [query, setQuery] = useState('');
const [debouncedQuery] = useDebounce(query, 600);
const { data: suggestions, isFetching } = useSearch(debouncedQuery);The query only sees the text once the user stops typing for 600 ms.
Cache each search on its own
export function useSearch(query: string) {
const normalizedQuery = query.trim();
return useQuery({
queryKey: queryKeys.product.suggestions(normalizedQuery),
queryFn: () => getProductSearchSuggestions(normalizedQuery),
enabled: normalizedQuery.length > 0,
staleTime: 1000 * 60 * 5,
});
}enabledmeans an empty box never sends a request.staleTimeof five minutes means searching "nike", then "nike air", then deleting back to "nike" shows the first results from cache, with no request.
Page numbers that don't flash
Each page is its own query. Without help, clicking "2" creates a key with no
data, isLoading turns true, and the skeleton replaces the table.
export function useOrderHistoryList(page: number) {
return useQuery({
queryKey: queryKeys.orderHistory.list(page),
queryFn: () => getOrderHistory(page, PAGE_SIZE),
placeholderData: keepPreviousData,
staleTime: 1000 * 60 * 5,
});
}v4 to v5
The keepPreviousData: true option was removed in TanStack Query v5. Use
placeholderData: keepPreviousData (imported from @tanstack/react-query)
instead.
isLoading vs isFetching
| Flag | True when | Show |
|---|---|---|
isLoading | First load, nothing cached to show | A skeleton |
isFetching | Any request in flight, including page changes | Dim the current rows |
const [page, setPage] = useState(1);
const { data, isLoading, isError, isFetching } = useOrderHistoryList(page);
if (isLoading) return <OrderHistoryTableSkeleton />;
if (isError || !data) return <OrderHistoryError />;
return (
<div className={isFetching ? 'opacity-70 transition-opacity' : ''}>
<DataTable columns={columns} data={data.orders} />
<Pagination page={data.page} total={data.total} onPageChange={setPage} />
</div>
);Read the page number from the response
The pagination uses data.page, not the page state. While page 2 loads,
the rows on screen are still page 1, and so is the highlighted number. They
never disagree.
Last updated on