Skip to main content

Data Fetching Hooks

Hooks for paginated queries, infinite scroll loading, and retry logic with exponential backoff. These hooks build on top of @tanstack/react-query and the template's API client.

usePaginatedQuery

Wraps useInfiniteQuery from React Query to provide cursor-based pagination against the template's REST API endpoints.

usePaginatedQuery<T>(options: UsePaginatedQueryOptions): UseInfiniteQueryResult

Options

OptionTypeDefaultDescription
endpointstring--API endpoint path (e.g., "/api/items")
limitnumber10Items per page
sortstring--Sort field name
order'asc' | 'desc'--Sort direction
filtersRecord<string, string | number | boolean | undefined>{}Additional query parameters
enabledbooleantrueWhether the query should execute

Return Values

Returns the standard React Query UseInfiniteQueryResult object with automatic page parameter handling. The query key includes all options for proper cache isolation.

Key BehaviorDescription
Query Key[endpoint, { limit, sort, order, ...filters }]
Page ParamStarts at 1, increments based on meta.totalPages
Next PageAutomatically calculated from lastPage.meta.page + 1
import { usePaginatedQuery } from '@/hooks/use-paginated-query';

function ItemList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = usePaginatedQuery<Item>({
endpoint: '/api/items',
limit: 20,
sort: 'createdAt',
order: 'desc',
filters: { status: 'approved' },
});

const allItems = data?.pages.flatMap((page) => page.data) ?? [];

return (
<div>
{allItems.map((item) => (
<ItemCard key={item.id} item={item} />
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
Load More
</button>
)}
</div>
);
}

useInfiniteLoading

Client-side infinite loading hook that progressively reveals items from an already-fetched array. Works with the template's paginationType setting from LayoutThemeContext.

useInfiniteLoading<T>(props: UseInfiniteLoadingProps<T>): UseInfiniteLoadingResult<T>

Props

PropTypeDefaultDescription
itemsT[]--Full array of items to paginate through
initialPagenumber--Starting page number
perPagenumberPER_PAGEItems shown per page increment

Return Values

PropertyTypeDescription
displayedItemsT[]Currently visible items (grows as pages load)
hasMorebooleanWhether more items are available
isLoadingbooleanLoading state during page advancement
errorError | nullError if page advancement fails
loadMore() => Promise<void>Trigger the next page of items

The hook only loads more items when paginationType is "infinite", making it safe to use alongside traditional pagination modes.

import { useInfiniteLoading } from '@/hooks/use-infinite-loading';

function InfiniteItemList({ allItems }) {
const { displayedItems, hasMore, isLoading, loadMore } = useInfiniteLoading({
items: allItems,
initialPage: 1,
perPage: 12,
});

return (
<div>
{displayedItems.map((item) => (
<ItemCard key={item.id} item={item} />
))}
{hasMore && (
<button onClick={loadMore} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Show More'}
</button>
)}
</div>
);
}

useRetry

Provides a generic retry mechanism with exponential backoff, jitter, and abort support. Useful for wrapping unreliable API calls.

useRetry(config?: Partial<RetryConfig>): UseRetryReturn

Configuration

OptionTypeDefaultDescription
maxRetriesnumber3Maximum number of retry attempts
retryDelaynumber1000Base delay in ms between retries
backoffMultipliernumber2Multiplier for exponential backoff
maxDelaynumber10000Maximum delay cap in ms
jitterbooleantrueAdd randomization (85-115%) to delay

Return Values

PropertyTypeDescription
retry<T>(fn: () => Promise<T>) => Promise<T>Execute a function with retry logic
reset() => voidAbort ongoing retries and reset state
stateRetryStateCurrent retry state

RetryState

PropertyTypeDescription
attemptnumberCurrent attempt number (0-based)
isRetryingbooleanWhether a retry is in progress
lastErrorError | nullMost recent error encountered
import { useRetry } from '@/hooks/use-retry';

function DataFetcher() {
const { retry, state } = useRetry({
maxRetries: 5,
retryDelay: 2000,
});

const fetchData = async () => {
const data = await retry(async () => {
const res = await fetch('/api/unstable-endpoint');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
});
setData(data);
};

return (
<div>
<button onClick={fetchData}>Fetch</button>
{state.isRetrying && <p>Retrying (attempt {state.attempt})...</p>}
</div>
);
}

useRetryOperation

A convenience wrapper that combines useRetry with a specific operation function and manages its result data and loading state.

useRetryOperation<T>(
operation: () => Promise<T>,
config?: Partial<RetryConfig>
): UseRetryOperationReturn<T>
PropertyTypeDescription
execute() => Promise<T>Run the operation with retry
reset() => voidReset state and abort
dataT | nullResult from last successful execution
loadingbooleanWhether the operation is in progress
attemptnumberCurrent retry attempt
isRetryingbooleanWhether actively retrying
lastErrorError | nullLast error encountered
const { execute, data, loading } = useRetryOperation(
() => fetch('/api/data').then((r) => r.json()),
{ maxRetries: 3 }
);

Key Design Patterns

Client-side vs Server-side Pagination

  • usePaginatedQuery -- Server-side pagination via React Query's useInfiniteQuery. Each page triggers a new API request.
  • useInfiniteLoading -- Client-side pagination. All items are loaded upfront; the hook progressively reveals them.

Retry Behavior

The useRetry hook skips retries for:

  • Abort errors (AbortError) -- The user or code intentionally cancelled
  • Client errors (4xx) -- These are typically not transient

Delay calculation: min(retryDelay * backoffMultiplier^attempt, maxDelay) * jitter


Summary Table

HookPurposeSource File
usePaginatedQueryServer-side infinite query paginationuse-paginated-query.ts
useInfiniteLoadingClient-side progressive item displayuse-infinite-loading.ts
useRetryGeneric retry with exponential backoffuse-retry.ts
useRetryOperationRetry wrapper for a specific async operationuse-retry.ts