Skip to content

ADR-0020: TanStack DB — Reactive Client Store

Last Updated: 2026-06-19 Status: Draft Context: Decksmith


Context

As Decksmith moves toward Phase 6 (Collection Management) and Phase 7 (Deck Builder), two UI patterns emerge that plain TanStack Query handles awkwardly:

Derived views / aggregations. The deck builder must show a mana curve (count of cards per CMC), a color distribution, a format validation result, and a collection coverage percentage — all derived from the same card list, re-derived on every add/remove. With plain TanStack Query these become either separate queries with complex cache synchronization, or useMemo chains that recompute from scratch on every render.

Optimistic mutations on lists. Adding a card to a deck or marking a collection entry should feel instant. onMutate/rollback in TanStack Query handles this for singleton objects, but becomes brittle when the optimistic state must propagate across multiple derived views simultaneously.

During a feasibility study (2026-06-19) TanStack DB was evaluated against the following constraints:

  • ADR-0016: apps/web communicates with apps/api over HTTP only — never imports packages/db or any server-only package.
  • ADR-0018: TanStack ecosystem coherence (Start, Query, Form, Table, Virtual already in use).
  • ADR-0019: packages/web-ui component architecture; components must not contain business logic.
  • Learning context: the primary learning goals for this project are backend and architecture, not additional frontend depth. Frontend investments must be justified.

Key feasibility finding: TanStack DB does not require ElectricSQL or direct database access. A queryCollection is loaded via a TanStack Query queryFn — meaning packages/api-client serves as the data source unchanged. ADR-0016 is fully preserved.


Current Decision

Adopt TanStack DB for Phase 6 and Phase 7. Defer adding the dependency until the start of Phase 6 (timeboxed spike, ~1 day).

Boundary rule (most important thing this ADR locks)

Data shapeSolution
Singletons / unique objects (user profile, user preferences)Plain TanStack Query (packages/query)
Lists, collections, derived/aggregate views (cards, deck contents, collection entries, stats)TanStack DB queryCollection + live queries

This rule is explicit and teachable. It prevents the two systems from leaking into each other's territory.

What TanStack DB adds (and what it does not add)

TanStack DB extends TanStack Query — it does not replace it. Three new primitives:

  • Collections: typed, normalized, client-side data stores loaded via queryFn (REST-compatible).
  • Live queries: reactive, SQL-like query engine (joins, filters, groupBy, aggregates) powered by differential dataflow — incremental recomputation (~0.7 ms to update one row in a 100k-item collection on M1 Pro). The deck builder's mana curve is a groupBy(cmc).count() live query.
  • Optimistic mutations: onInsert/onUpdate/onDelete handlers with automatic rollback; optimistic state is overlaid on synced data and propagates to all live queries instantly.

Integration shape

queryCollection({
  queryKey: ['collection-entries'],
  queryFn: () => apiClient.collection.listEntries(),   // packages/api-client — ADR-0016 preserved
  onInsert: (tx) => apiClient.collection.addEntry(tx),
  onUpdate: (tx) => apiClient.collection.updateEntry(tx),
  onDelete: (tx) => apiClient.collection.removeEntry(tx),
})

Live queries tap the collection:

ts
// Mana curve — free, incremental, no useMemo
const manaCurve = useLiveQuery((q) =>
  q.from({ cards: deckCardsCollection }).groupBy('cmc').select({ cmc: q.cmc, count: q.count() })
);

When to revisit / install

  • At the start of Phase 6 (Collection Management): install @tanstack/db and @tanstack/react-db, run a ~1-day spike (one collection + one live query + one optimistic mutation against a mocked API endpoint). Write findings to this ADR.
  • Re-evaluate if TanStack DB has not reached ≥ 1.0 by Phase 6 start — if still deeply in beta, defer a second time and document why.
  • Do not install the dependency before Phase 6 starts.

What we explicitly do NOT do

  • Do not install @tanstack/db or @tanstack/react-db today.
  • Do not migrate useUser or useUserPreferences — they are singletons; plain Query is correct.
  • Do not create any collection before Phase 3 (Scryfall) and Phase 6 are started.
  • Do not use TanStack DB for server state that is genuinely singleton (auth state, user profile, feature flags).

Rationale

Why TanStack DB and not more useMemo / manual cache synchronization?

Derived views (mana curve, color distribution, coverage) recompute from the same underlying list. With useMemo they recompute from scratch on every render cycle; with multiple useQuery hooks they require coordinated invalidation. The differential dataflow engine in TanStack DB recomputes only the affected rows — O(change) not O(collection). For a deck with 100 cards this is imperceptible; for a collection with 10,000 entries it is the difference between a 0.7 ms update and a 70 ms rerender freeze.

Why not plain TanStack Query optimistic mutations?

onMutate/setQueryData/cancelQueries works for singleton objects. For list mutations that must propagate across multiple derived views simultaneously (add a card → update the list, the mana curve, the color distribution, the format validation, the collection coverage all at once), the manual cache surgery becomes error-prone and hard to test. TanStack DB's optimistic layer handles this by design — mutations propagate to all live queries over the collection automatically.

Why the boundary rule (singletons → Query, lists → DB)?

Without an explicit rule, engineers will reach for whichever API they learned most recently. The boundary is meaningful: singletons have one source of truth and no derived views; lists have multiple consumers and aggregate projections. It is also aligned with the original TanStack DB design intent.

Why defer?

  • TanStack DB is pre-1.0 at decision time — API churn is expected. Deferring to Phase 6 gives time for the library to stabilize (likely ≥ 1.0 by then given the pace of the TanStack ecosystem).
  • Collection and Deck Builder both depend on Phase 3 (Scryfall integration, not yet started). There is no write-heavy list UI to optimize today. Carrying a beta dependency with no active use competes with higher-priority learning goals (backend, auth, Scryfall sync).
  • The spike at Phase 6 start validates the SSR/hydration wrinkle (collections are client-first; TanStack Start SSR must prime the initial load via a route loader, then hand off to the collection client-side). Better to discover that in a throwaway branch than mid-feature.

Ecosystem coherence. TanStack Query is already the data layer. TanStack DB extends it without replacing it — the mental model delta is smaller than adopting an entirely different system (Zustand for server data, Jotai, SWR, etc.). Devtools, query keys, and error handling patterns carry over directly.


Trade-offs

Benefits:

  • Incremental derived views (mana curve, color breakdown, coverage) with ~0.7 ms update time.
  • Optimistic mutations that propagate across all live queries automatically, with automatic rollback.
  • packages/api-client used unchanged as queryFn/mutationFn — ADR-0016 fully preserved.
  • Stays in the TanStack ecosystem — shared devtools, consistent API surface, coherent mental model.
  • The boundary rule (singletons vs. collections) is explicit, teachable, and enforceable in review.

Costs:

  • Second mental model alongside plain TanStack Query — engineers must know when to use which.
  • Additional frontend depth that does not advance the stated backend/architecture learning goals.
  • packages/test-utils must be extended to cover live queries and optimistic transactions in tests (MSW covers the HTTP layer; collection state assertions need a new pattern).
  • Some boilerplate per collection (queryFn + three mutation handlers + collection definition).

Risks:

  • Beta API churn@tanstack/db is pre-1.0 at decision time; handler signatures or collection configuration may change before Phase 6. Mitigation: deferred until Phase 6; likely ≥ 1.0 by then. The spike validates the actual API before committing to it. If still unstable, defer a second time.
  • SSR / hydration wrinkle — TanStack Start uses server-side rendering; TanStack DB collections are client-first. The initial page load must be primed by a route loader (standard queryFn), then the collection takes over client-side. Mishandled, this causes a hydration mismatch. Mitigation: explicitly validated during the Phase 6 spike before any production use.
  • Test patterns for live queriesuseLiveQuery is reactive and stateful; existing MSW + createQueryWrapper patterns cover the HTTP layer but not collection state transitions. Mitigation: defined during the spike; likely a createCollectionWrapper utility in packages/test-utils paralleling the existing createQueryWrapper.

Evolution History

2026-06-19: Initial decision (adopt-but-defer)

  • Feasibility study completed: docs reviewed (overview, live queries, reference), existing data layer reviewed (packages/query, packages/api-client, context.tsx).
  • Key finding: queryCollection is REST-compatible — no ElectricSQL required. ADR-0016 is preserved.
  • Decision: adopt for Phase 6 / Phase 7; defer dependency install to Phase 6 spike; boundary rule locked (singletons → Query, lists/derived views → DB).
  • Status set to Draft (direction decided, implementation deferred — no code yet in use).

References

Built with VitePress