BackOpenStatus Logo

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, live for stable cache keys)
  • Initial cursorDate.now() (most recent data first)
  • Page paramsgetNextPageParam / getPreviousPageParam from nextCursor / prevCursor
  • CachingkeepPreviousData for 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.

GitHubXBluesky

Powered by OpenStatus