Workbench

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.

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

NavbarSearch.tsx
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

useSearch.ts
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, 
  });
}
  • enabled means an empty box never sends a request.
  • staleTime of 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.

useOrderHistoryList.ts
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

FlagTrue whenShow
isLoadingFirst load, nothing cached to showA skeleton
isFetchingAny request in flight, including page changesDim the current rows
OrderHistoryDesktop.tsx (simplified)
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

On this page