Magic: The Gathering example
A full DataTable integration against a real API (scryfall.com) — server-driven sorting, filtering, and global search synced to the URL, a row-detail Drawer, and real loading/error/retry states.
Everything on the main DataTable page runs against small, in-memory arrays.
This page is the opposite: every row below comes from a live network request to
Scryfall (api.scryfall.com), a free, open Magic: The
Gathering card database — so the loading, error, sorting, filtering, and search you see are the
real thing, not a mock. Sorting, filters, and the search box also sync to this page's own URL —
try changing a filter, then reload.
Rows per page
1
What's server-driven
Scryfall genuinely sorts, filters, and searches server-side — verified directly against the live API, not assumed from its docs:
- Search (
enableGlobalFilter, debounced) is appended as a bare term to Scryfall's ownqsearch syntax, which matches on name (and oracle text). - Column filters are real search-syntax clauses, not client-side narrowing of an already-loaded
page: the type-line filter maps to
t:"…", the multi-select colors filter to an OR-grouped(c:w or c:u), rarity tor:<value>, and the mana-value/price number filters tocmc>=3/usd<=5— Scryfall's numeric fields take a comparison operator with no colon (confirmed live:cmc:>=3, combining both, is invalid and silently ignored with a warning). The released-date filter is Scryfall'sdate/yearkeyword the same way, always sending both bounds (date>=2015-01-01 date<=2020-12-31). - Sorting covers seven columns for real —
name,colors(→order=color),rarity, mana value,set, release date, and price all genuinely reorder results server-side, a real step up from the kit's earlier Pokémon example, where onlynamedid.enableMultiSortis deliberately off: Scryfall'sorder/dironly accept a single field, so a multi-sort UI would silently drop every secondary key. - Pagination has a real, known total — Scryfall's search response includes
total_cards, wired straight intorowCountfor a genuine "X–Y of Z" footer and last-page jump. The one wrinkle: Scryfall's page size is a fixed 175 rows, not configurable, sopageSizeOptionsoffers exactly that one real value instead of a picker implying a choice that doesn't exist.
One request, not two
Unlike the kit's earlier Pokémon example — whose list endpoint only returned brief rows, requiring
a second per-row detail fetch to hydrate every filterable field — Scryfall's search response
already returns the full card object: mana cost, oracle text, prices, legalities, and every
image size. Loading a page is a single useQuery call, and the row-detail Drawer needs no fetch of
its own either.
Caching and validation
Data fetching runs through @tanstack/react-query (useQuery + keepPreviousData) instead of
hand-rolled useEffect + loading/error state — the same recipe the
Query integration section on the main page documents in a
fenced code block. Keeping the previous page's rows around while the next page loads is also what
makes DataTable's own loading prop (bound to isFetching) correctly dim the existing rows
instead of resetting to a skeleton.
The card shape is a single zod schema (CardSchema), not a hand-written type — columnHelperFor
derives fully-typed columns straight from it, and every page's response is .parse()d against it
before the query resolves. That's real runtime validation of a third-party API's response, not just
type inference: if Scryfall's shape ever drifts, it surfaces as a normal DataTable error state
instead of a silent crash. One real API nuance this schema (and its column cells) accounts for:
double-faced cards omit image_uris/mana_cost/colors at the top level entirely — those only
exist per-face on card_faces[] — while split cards keep them at the top level. Falling back to
card_faces[0] when the top-level field is missing covers both shapes.
URL-synced state
Sorting, column filters, and the global search box all sync to this page's own URL query string —
mirroring the one other URL-synced view in this app, the
changelog timeline's apps/docs/lib/changelog-url.ts codec +
useSearchParams/router.replace component. A pure magic-data-table-url.ts codec encodes/decodes
the table's state, validating every token on the way in so a hand-edited or stale URL can never
break rendering. Pagination is deliberately excluded — Scryfall's fixed page size isn't meaningful
to bookmark against.
Row details
Clicking a row (or its trailing view button — a row click is a convenience, never the only way to
act on a row) opens a Drawer with the card's full detail, instantly, including rules text,
prices in three currencies, and legality across seven common formats. enableColumnVisibility and
responsive="stack" are both on, so narrowing your viewport (or the window) switches to the
stacked-card layout and its "Table options" drawer — driven by the exact same live data. Column
widths persist across reloads via useLocalStorage, and selecting rows enables an "Export
decklist" action that builds a plain-text decklist from the current selection.
Source: apps/docs/stories/magic-data-table.tsx.
DataTable
Smart, data-driven table on TanStack Table — sorting, filtering, pagination, selection, and a responsive stacked-card mode, rendered through the Table primitive.
Timeline
Vertical, chronological list of events — activity feed, audit log, changelog. Pure Server Component with namespaced parts.