Skip to main content

usePaginatedQuery

Overview

usePaginatedQuery wraps TanStack React Query's useInfiniteQuery to provide a standardized interface for fetching paginated data from the template's API endpoints. It handles page parameter management, automatic next-page detection, and integrates with the shared fetcherPaginated utility from the API client. Use this hook whenever you need to load data in pages with "load more" or infinite scroll patterns.

Import

import { usePaginatedQuery } from "@/hooks/use-paginated-query";

API Reference

Parameters

The hook accepts a single options object:

ParameterTypeRequiredDefaultDescription
endpointstringYes--The API endpoint path to fetch from (e.g., "/api/items").
limitnumberNo10Number of items to fetch per page.
sortstringNoundefinedField name to sort results by.
order"asc" | "desc"NoundefinedSort direction. Only applied when sort is also provided.
filtersRecord<string, string | number | boolean | undefined>No{}Key-value pairs for query parameter filtering. undefined values are excluded.
enabledbooleanNotrueWhen false, the query will not execute. Useful for conditional fetching.

Generic Type Parameter

ParameterDescription
TThe type of individual items in the paginated response.

Return Value

Returns the full UseInfiniteQueryResult from TanStack React Query, which includes:

PropertyTypeDescription
dataInfiniteData<PaginatedResponse<T>>The accumulated pages of data. Access items via data.pages[n].data.
fetchNextPage() => Promise<...>Fetches the next page of results.
hasNextPagebooleanWhether more pages are available.
isFetchingNextPagebooleanWhether the next page is currently being fetched.
isLoadingbooleanWhether the initial page is loading.
isErrorbooleanWhether an error occurred.
errorError | nullThe error object if the query failed.
refetch() => Promise<...>Manually re-fetches all pages.

The PaginatedResponse<T> type (when successful) has the shape:

{
success: true;
data: T[];
meta: {
page: number;
totalPages: number;
total: number;
limit: number;
};
}

Usage Examples

Basic Usage

import { usePaginatedQuery } from "@/hooks/use-paginated-query";

interface Item {
id: string;
name: string;
slug: string;
}

function ItemList() {
const {
data,
fetchNextPage,
hasNextPage,
isLoading,
isFetchingNextPage,
} = usePaginatedQuery<Item>({
endpoint: "/api/items",
limit: 20,
});

if (isLoading) return <div>Loading...</div>;

const items = data?.pages.flatMap((page) =>
page.success ? page.data : []
) ?? [];

return (
<div>
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? "Loading..." : "Load More"}
</button>
)}
</div>
);
}

Advanced Usage

import { usePaginatedQuery } from "@/hooks/use-paginated-query";
import { useDebounceValue } from "@/hooks/use-debounced-value";
import { useState } from "react";

interface Item {
id: string;
name: string;
category: string;
}

function FilteredItemList() {
const [search, setSearch] = useState("");
const [category, setCategory] = useState<string | undefined>(undefined);
const debouncedSearch = useDebounceValue(search, 300);

const {
data,
fetchNextPage,
hasNextPage,
isLoading,
} = usePaginatedQuery<Item>({
endpoint: "/api/items",
limit: 15,
sort: "name",
order: "asc",
filters: {
search: debouncedSearch || undefined,
category,
},
enabled: true,
});

const items = data?.pages.flatMap((page) =>
page.success ? page.data : []
) ?? [];

return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search items..."
/>
<select
value={category ?? ""}
onChange={(e) => setCategory(e.target.value || undefined)}
>
<option value="">All Categories</option>
<option value="tools">Tools</option>
<option value="services">Services</option>
</select>

{isLoading ? (
<p>Loading...</p>
) : (
items.map((item) => <div key={item.id}>{item.name}</div>)
)}

{hasNextPage && (
<button onClick={() => fetchNextPage()}>Load More</button>
)}
</div>
);
}

Integration Patterns

The hook constructs a query key from [endpoint, { limit, sort, order, ...filters }], so TanStack React Query automatically refetches when any of these parameters change. It uses fetcherPaginated from @/lib/api/api-client, which handles authentication headers, base URL resolution, and response parsing. The getNextPageParam function checks the meta.page and meta.totalPages fields in the PaginatedResponse to determine whether more pages exist.

Best Practices

  • Flatten pages for rendering using data.pages.flatMap(page => page.success ? page.data : []) to get a single array of items.
  • Set enabled: false when required filter values are not yet available to prevent unnecessary initial requests.
  • Use filters with undefined values for optional parameters -- they are excluded from the query automatically.
  • Combine with useDebounceValue for search and filter inputs to avoid excessive API calls while the user is typing.
  • Keep limit reasonable (10--50 items per page) to balance between network efficiency and user experience.
  • useDebounceValue -- Debounce filter/search inputs before passing them to usePaginatedQuery.
  • useInfiniteLoading -- Scroll-based infinite loading that can build on paginated query results.
  • useFilters -- Manages filter state that feeds into paginated queries.