Data Fetching
Data fetching is powered by TanStack React Query using useInfiniteQuery for cursor-based pagination. A factory function creates the query options so you don't have to wire up cursors, serialization, or caching manually.
Query Options Factory
createDataTableQueryOptions generates infiniteQueryOptions for your table. It handles cursor management, search param serialization, and SuperJSON deserialization:
import { createDataTableQueryOptions } from "@/lib/data-table";
const _dataOptions = createDataTableQueryOptions<ColumnSchema[], MyMeta>({
queryKeyPrefix: "my-table",
apiEndpoint: "/my-table/api",
searchParamsSerializer: searchParamsSerializer,
});
export const dataOptions = (search: SearchParamsType) =>
_dataOptions(search as unknown as Record<string, unknown>);The factory configures:
- Query key — derived from serialized search params (excludes
cursor,direction,uuid,livefor stable cache keys) - Initial cursor —
Date.now()(most recent data first) - Page params —
getNextPageParam/getPreviousPageParamfromnextCursor/prevCursor - Caching —
keepPreviousDatafor smooth filter transitions, 5-minute stale time, no refetch on window focus
useInfiniteQuery Pattern
function DataTableContent() {
const search = useFilterState<FilterState>();
const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery(
dataOptions(search),
);
// Flatten pages into a single array
const flatData = React.useMemo(
() => data?.pages?.flatMap((page) => page.data ?? []) ?? [],
[data?.pages],
);
// Derive column filters from state (exclude non-filter fields)
const { sort, cursor, direction, uuid, live, size, ...filter } = search;
const defaultColumnFilters = React.useMemo(() => {
return Object.entries(filter)
.map(([key, value]) => ({ id: key, value }))
.filter(({ value }) => {
if (value === null || value === undefined) return false;
if (Array.isArray(value) && value.length === 0) return false;
return true;
});
}, [filter]);
return (
<DataTableInfinite
data={flatData}
defaultColumnFilters={defaultColumnFilters}
defaultColumnSorting={sort ? [sort] : undefined}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isFetching={isFetching}
// ...
/>
);
}When the user scrolls to the bottom, DataTableInfinite calls fetchNextPage. React Query fetches the next page using the nextCursor from the last page's response and appends it to data.pages.
Server Prefetch
Server-side prefetching with React Query's HydrationBoundary avoids a loading spinner on first render:
// page.tsx
import { getQueryClient } from "@/providers/get-query-client";
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
export default async function Page({ searchParams }) {
const search = await searchParamsCache.parse(searchParams);
const queryClient = getQueryClient();
await queryClient.prefetchInfiniteQuery(dataOptions(search));
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Client />
</HydrationBoundary>
);
}The query is prefetched on the server and dehydrated into the HTML. On the client, useInfiniteQuery picks up the cached data immediately — no extra network request.
Powered by OpenStatus
