|Kit
DataTable

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.

Full page example

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 own q search 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 to r:<value>, and the mana-value/price number filters to cmc>=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's date/year keyword 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 only name did. enableMultiSort is deliberately off: Scryfall's order/dir only 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 into rowCount for 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, so pageSizeOptions offers 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.

On this page