Server Integration Guide
REST API Integration
TableCraft is data-source agnostic. You manage fetching in your own component and pass the resulting data to ClientSideTable. The examples below show the two most common REST patterns.
Basic Pattern
Use useState to hold data, pagination metadata, and loading state. A useEffect triggers the fetch whenever the current page or page-size changes. Pass isQueryPagination to tell TableCraft that pagination is controlled externally.
"use client";
import { useState, useEffect } from "react";
import { ClientSideTable } from "@your-scope/tablecraft";
import { columns } from "./columns";
interface Meta {
current_page: number;
last_page: number;
per_page: number;
total: number;
}
export default function UsersTable() {
const [data, setData] = useState<User[]>([]);
const [meta, setMeta] = useState<Meta | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(15);
useEffect(() => {
setLoading(true);
fetch(`/api/users?page=${page}&per_page=${perPage}`)
.then((res) => res.json())
.then((json) => {
setData(json.data);
setMeta(json.meta);
})
.finally(() => setLoading(false));
}, [page, perPage]);
return (
<ClientSideTable
columns={columns}
data={data}
isLoading={loading}
isQueryPagination
currentPage={meta?.current_page ?? 1}
lastPage={meta?.last_page ?? 1}
perPage={perPage}
total={meta?.total ?? 0}
onPageChange={setPage}
onPerPageChange={(value) => {
setPerPage(value);
setPage(1);
}}
/>
);
}Full Server-Side Pattern
When search and filters also live on the server, wrap the fetch call in useCallback so it can be triggered both from pagination changes and from filter/search change handlers without duplicating logic.
"use client";
import { useState, useEffect, useCallback } from "react";
import { ClientSideTable } from "@your-scope/tablecraft";
import { columns } from "./columns";
interface Filters {
status?: string;
role?: string;
}
export default function UsersTable() {
const [data, setData] = useState<User[]>([]);
const [meta, setMeta] = useState<Meta | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(15);
const [search, setSearch] = useState("");
const [filters, setFilters] = useState<Filters>({});
const fetchData = useCallback(async () => {
setLoading(true);
try {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage),
...(search ? { search } : {}),
...filters,
});
const res = await fetch(`/api/users?${params.toString()}`);
const json = await res.json();
setData(json.data);
setMeta(json.meta);
} finally {
setLoading(false);
}
}, [page, perPage, search, filters]);
useEffect(() => {
fetchData();
}, [fetchData]);
return (
<ClientSideTable
columns={columns}
data={data}
isLoading={loading}
isQueryPagination
currentPage={meta?.current_page ?? 1}
lastPage={meta?.last_page ?? 1}
perPage={perPage}
total={meta?.total ?? 0}
onPageChange={(p) => setPage(p)}
onPerPageChange={(value) => {
setPerPage(value);
setPage(1);
}}
onSearchChange={(value) => {
setSearch(value);
setPage(1);
}}
onFiltersChange={(value) => {
setFilters(value as Filters);
setPage(1);
}}
/>
);
}React Query Integration
React Query handles caching, background refetching, and loading/error states for you. Pass the query result directly into ClientSideTable.
"use client";
import { useQuery } from "@tanstack/react-query";
import { ClientSideTable } from "@your-scope/tablecraft";
import { columns } from "./columns";
interface PageParams {
page: number;
perPage: number;
}
async function fetchUsers({ page, perPage }: PageParams) {
const res = await fetch(`/api/users?page=${page}&per_page=${perPage}`);
if (!res.ok) throw new Error("Failed to fetch users");
return res.json();
}
export default function UsersTable() {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(15);
const { data, isLoading } = useQuery({
queryKey: ["users", { page, perPage }],
queryFn: () => fetchUsers({ page, perPage }),
placeholderData: (prev) => prev,
});
return (
<ClientSideTable
columns={columns}
data={data?.data ?? []}
isLoading={isLoading}
isQueryPagination
currentPage={data?.meta?.current_page ?? 1}
lastPage={data?.meta?.last_page ?? 1}
perPage={perPage}
total={data?.meta?.total ?? 0}
onPageChange={setPage}
onPerPageChange={(value) => {
setPerPage(value);
setPage(1);
}}
/>
);
}SWR Integration
SWR offers a lightweight alternative to React Query with an identical usage pattern for TableCraft.
"use client";
import { useState } from "react";
import useSWR from "swr";
import { ClientSideTable } from "@your-scope/tablecraft";
import { columns } from "./columns";
async function fetcher(url: string) {
const res = await fetch(url);
if (!res.ok) throw new Error("Network response was not ok");
return res.json();
}
export default function UsersTable() {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(15);
const { data, isLoading } = useSWR(
`/api/users?page=${page}&per_page=${perPage}`,
fetcher,
{ keepPreviousData: true }
);
return (
<ClientSideTable
columns={columns}
data={data?.data ?? []}
isLoading={isLoading}
isQueryPagination
currentPage={data?.meta?.current_page ?? 1}
lastPage={data?.meta?.last_page ?? 1}
perPage={perPage}
total={data?.meta?.total ?? 0}
onPageChange={setPage}
onPerPageChange={(value) => {
setPerPage(value);
setPage(1);
}}
/>
);
}Cursor-Based Pagination
For APIs that use opaque cursors instead of page numbers (GraphQL Relay connections, Stripe list endpoints, keyset pagination), use isCursorPagination instead of isQueryPagination. The pagination footer renders only Prev/Next buttons — no numbered pages — because cursor-based APIs do not support random access.
GraphQL Relay Example
"use client";
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { ClientSideTable } from "@your-scope/tablecraft";
import type { CursorPaginationData } from "@your-scope/tablecraft";
import { columns } from "./columns";
const USERS_QUERY = `
query Users($first: Int, $after: String, $before: String) {
users(first: $first, after: $after, before: $before) {
edges { node { id name email role } }
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`;
export default function UsersTable() {
const [pageSize, setPageSize] = useState(15);
const [cursors, setCursors] = useState<{
after?: string; before?: string
}>({});
const { data, isLoading } = useQuery({
queryKey: ["users", pageSize, cursors],
queryFn: () =>
graphqlClient.request(USERS_QUERY, {
first: pageSize,
...cursors,
}),
placeholderData: (prev) => prev,
});
const connection = data?.users;
const cursorPaginationData: CursorPaginationData = {
pageInfo: {
hasNextPage: connection?.pageInfo.hasNextPage ?? false,
hasPreviousPage: connection?.pageInfo.hasPreviousPage ?? false,
startCursor: connection?.pageInfo.startCursor,
endCursor: connection?.pageInfo.endCursor,
totalCount: connection?.totalCount,
},
onNextPage: () =>
setCursors({ after: connection?.pageInfo.endCursor }),
onPreviousPage: () =>
setCursors({ before: connection?.pageInfo.startCursor }),
onPageSizeChange: (size) => {
setPageSize(size);
setCursors({});
},
pageSize,
};
return (
<ClientSideTable
columns={columns}
data={connection?.edges.map(e => e.node) ?? []}
isLoading={isLoading}
isCursorPagination
cursorPaginationData={cursorPaginationData}
/>
);
}Expected Cursor API Response Format
Cursor-based APIs typically return an edges array and a pageInfo object. TableCraft requires only the four pageInfo fields shown below. The optional totalCount is displayed as a record count label when provided.
{
"edges": [
{ "node": { "id": "1", "name": "Alice Johnson" }, "cursor": "abc123" },
{ "node": { "id": "2", "name": "Bob Smith" }, "cursor": "def456" }
],
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "abc123",
"endCursor": "def456"
},
"totalCount": 208
}Expected Offset API Response Format
For offset-based pagination, TableCraft expects a Laravel-style paginated response shape. The data array contains the records for the current page. The meta object carries pagination context. The links object is optional and ignored by TableCraft but included here for completeness since many APIs return it.
{
"data": [
{ "id": 1, "name": "Alice Johnson", "email": "alice@example.com", "role": "admin" },
{ "id": 2, "name": "Bob Smith", "email": "bob@example.com", "role": "editor" }
],
"meta": {
"current_page": 1,
"last_page": 14,
"per_page": 15,
"total": 208,
"from": 1,
"to": 15
},
"links": {
"first": "https://api.example.com/users?page=1",
"last": "https://api.example.com/users?page=14",
"prev": null,
"next": "https://api.example.com/users?page=2"
}
}meta object are required by TableCraft: meta.current_page, meta.last_page, meta.per_page, and meta.total. The remaining fields (from, to) and the entire links object are optional and can be omitted if your API does not return them.