Tulip Logo IconTulip
Query

Table Integration

Connect query controllers to data-table strategies without duplicating query state.

Data tables render rows and table interactions. Query owns filters, sorting, pagination, schemas, and query input.

Server Pagination Table

"use client";

import { useSuspenseQuery } from "@tanstack/react-query";
import { offsetPagination } from "@tulip-systems/query";
import { useFilters, usePagination, useSorting } from "@tulip-systems/query/client";
import {
  createTableConfig,
  DataTable,
  TableConfigProvider,
  usePaginationStrategy,
} from "@tulip-systems/data-tables/client";
import { useDeferredValue } from "react";

export function ProjectTableProvider({ children }: { children: React.ReactNode }) {
  const pagination = usePagination(offsetPagination);
  const filtering = useFilters(projectFilters);
  const sorting = useSorting(projectSorting);

  const input = useDeferredValue({
    pagination: pagination.input,
    filters: filtering.input,
    sorts: sorting.input,
  });

  const { data } = useSuspenseQuery(orpc.projects.list.queryOptions({ input }));

  const strategy = usePaginationStrategy({
    pagination,
    total: data.pagination.total,
  });

  const config = createTableConfig({
    queryData: data.data,
    columns: projectColumns,
    strategy,
    pagination,
    filtering,
    sorting,
  });

  return <TableConfigProvider config={config}>{children}</TableConfigProvider>;
}

Render table UI under the provider:

<ProjectTableProvider>
  <ProjectToolbar />
  <DataTable />
</ProjectTableProvider>

Infinite Table

Use the same dataset input shape and feed page params into pagination.page.

const response = useSuspenseInfiniteQuery(
  orpc.tasks.list.infiniteOptions({
    initialPageParam: 0,
    input: (page: number) => ({
      ...input,
      pagination: { ...input.pagination, page },
    }),
    getNextPageParam: ({ pagination }) => pagination.nextPage,
    getPreviousPageParam: ({ pagination }) => pagination.previousPage,
  }),
);

const strategy = useInfiniteStrategy({
  pagination,
  total: response.data.pages.at(-1)?.pagination.total,
  fetchNextPage: response.fetchNextPage,
  isFetching: response.isFetching,
  isFetchingNextPage: response.isFetchingNextPage,
});

useInfiniteStrategy(...) receives the pagination controller. It does not receive or expose cursor state.

Toolbars

Toolbar controls should use dataset filter descriptors.

import { resolveFilterDescriptors } from "@tulip-systems/query";
import { FilterSearch } from "@tulip-systems/query/client";

const filters = resolveFilterDescriptors(projectFilters);

export function ProjectToolbar() {
  return <FilterSearch placeholder="Zoeken..." filter={filters.search} />;
}

Dependent option queries should read resolved filters from table context.

const table = useTableConfigContext();
const input = useDeferredValue({ filters: table.filtering?.input ?? {} });

const { data } = useSuspenseQuery(orpc.projects.filterCustomers.queryOptions({ input }));

Local Collections

TanStack DB load-subset helpers use the same dataset language.

const { page, limit } = parseLoadSubsetPagination(ctx.meta?.loadSubsetOptions);
const parsedSorts = parseLoadSubsetSorting(ctx.meta?.loadSubsetOptions);
const parsedFilters = parseLoadSubsetFilters(ctx.meta?.loadSubsetOptions);

const { data, pagination } = await orpc.tasks.getCollection.call({
  pagination: { page, limit },
  parsedSorts,
  parsedFilters,
});

This keeps local-first and server-first flows aligned around page, limit, filters, and sorts.

On this page