# Data Fetching

> TanStack React Query integration with useInfiniteQuery, query options factory, and server prefetching

Source: https://data-table.openstatus.dev/docs/data-fetching · Docs index: https://data-table.openstatus.dev/llms.txt · Full docs: https://data-table.openstatus.dev/llms-full.txt

Data fetching is powered by [TanStack React Query](https://tanstack.com/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. See the [/infinite](/infinite) demo for a working example.

### Query Options Factory

`createDataTableQueryOptions` generates `infiniteQueryOptions` for your table. It handles cursor management, search param serialization, and [SuperJSON](https://github.com/flightcontrolhq/superjson) deserialization:

```ts
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 cursor** — `Date.now()` (most recent data first)
- **Page params** — `getNextPageParam` / `getPreviousPageParam` from `nextCursor` / `prevCursor`
- **Caching** — `keepPreviousData` for smooth filter transitions, 5-minute stale time, no refetch on window focus

### useInfiniteQuery Pattern

```tsx
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:

```tsx
// 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.
