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.