TanStack Stack (Query + Router + Table)
Status: Active
Last Updated: 2026-08-27
Category: Frontend - Architecture
Prerequisites: kb/frontend/website-rebuild.md (rebuild context), kb/frontend/component-library-notes.md (token usage), kb/frontend/routing-patterns.md (file-based routing)
Tags: tanstack, tanstack-query, tanstack-router, tanstack-table, react, typescript, spa, cache, ssr
Summary
TanStack Query, Router, and Table form the UI glue for fogserv.cloud's React 19 SPA. This article documents how each library handles asynchronous state, routing, and tabular data so agentic dashboards stay consistent with the live GitOps data sources (Prisma, Forgejo actions, mail webhooks). The patterns here are followed across all new pages in src/routes/ and admin components.
Context / Why This Matters
The site uses a single QueryClient as the source of truth for server-derived state (Post, User, Tag, webhook metrics), while TanStack Router handles URL-level navigation with SSR-safe loaders. Misaligning these layers — e.g., fetching inside a component rather than a loader — breaks SSR consistency, duplicates cache keys, and produces hydration errors. TanStack Table is adopted only when sorting/paging complexity exceeds a plain HTML table (see component-library-notes.md).
This article consolidates practices from the initial build (January 2026), updates them for v5+ APIs, and links to the routing patterns established in routing-patterns.md.
Implementation / Core Content
TanStack Query — Data Layer
Single
QueryClientinsrc/routes/__root.tsx. All resources (posts, subscribers, analytics) share it.Query keys are structured, not arbitrary strings:
['posts', { status }],['kb', { slug }],['analytics', 'opens']. This prevents key collision across routes.staleTime/gcTimetuned per resource:- Static KB articles:
staleTime: Infinity(rare updates). - Dashboard metrics:
staleTime: 30000(30s refresh). - Admin lists:
staleTime: 5000(fast iter).
- Static KB articles:
select+metabefore passing data to components:const data = useQuery({ queryKey: ['posts', { status: 'draft' }], queryFn: () => fetchPosts({ status: 'draft' }), select: (posts) => posts.map(p => ({ id: p.id, title: p.title, slug: p.slug })), })Mutations go through route actions or
useMutation, withinvalidateQueriesimmediately after:await mutation.mutateAsync(values) queryClient.invalidateQueries({ queryKey: ['posts'] })
TanStack Router — Routing Layer
- Route tree uses file-based flat naming (
admin.new-post.tsx,kb_.$.tsx). See routing-patterns.md for full naming rules. - Loaders run before render on both client and server (SSR). They return serializable JSON; never raw
Responseobjects. beforeLoadfor auth guards (e.g.,isAdmin). Never put auth checks insideuseEffect— that creates a flash of unauthorized UI.- Navigation uses typed
<Link>anduseNavigate()with generatedrouteTree.gen.tsfor compile-time path validation.
TanStack Table — Tabular Data
Adopted only for the admin post list (PostListRow, PostsSection) and analytics dashboards where paginated, sortable views are required. Plain <table> + Tailwind is preferred for KB article cards (kb.index.tsx).
Key configuration:
getCoreRowModel(),getPaginationRowModel()for standard tables.manualPaginationwhen server-side pagination is required (high-volume analytics).- Sorting/filtering state mirrored to URL search params when it affects the route loader.
Practical Examples
Example 1: Route loader with typed return
// src/routes/kb_.$.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/kb/$slug')({
loader: async ({ params }) => {
const article = await getKbArticle(params.slug)
if (!article) throw new Error('Not found')
return { article }
},
component: KbArticle,
})
function KbArticle() {
const { article } = Route.useLoaderData()
return <article><h1>{article.title}</h1>...</article>
}
Example 2: Query + mutation pattern for admin actions
// Admin post creation (POST /api/posts)
const mutation = useMutation({
mutationFn: (postData) => api.createPost(postData),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
})
const handleCreate = async (form: PostForm) => {
await mutation.mutateAsync(form)
navigate({ to: '/admin' })
}
Example 3: Client-only widget isolation
Any component using browser APIs (localStorage, window) must be isolated at the leaf:
'use client';
export function ThemeToggle() { ... }
Used from a server component like any other element; isolation stays at the leaf per component-library-notes.md.
Common Pitfalls & Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Loader data undefined after navigation | Fetch inside component instead of loader; non-serializable return | Move fetch to loader; return JSON-safe values |
No overload matches 'to=... TypeScript error |
routeTree.gen.ts stale or path typo'd |
Regenerate (bun run dev); copy path exactly from createFileRoute |
| Hydration mismatch warning | Client-only API accessed during SSR render | Isolate in 'use client' component or useEffect |
| Duplicate fetch logic across components | Data fetched below route level | Hoist to loader/service; pass via props or loaderData |
| Auth bypassed on direct URL hit | Guard only in useEffect or component mount |
Move to beforeLoad |
| Query key collision between admin and public list | Shared generic key like ['posts'] |
Add filter params: ['posts', { status: 'draft' }] |
| Table pagination breaks on SSR | Client-side pagination state not mirrored in URL | Use manualPagination + mirror to URL search params |
Next Steps / Ops Actions
- Update
QueryClientdefaults forretry,suspense, anderrorBoundarypolicies to support agent telemetry dashboards. - Migrate remaining plain tables (analytics cards) to TanStack Table only when pagination exceeds 20 rows.
- Document any custom loader patterns in
kb/frontend/website-rebuild.mdchange log. - After the
@thememigration lands (tailwind-v4-migration.md), verify component token consumption stays aligned.
Sources & Related Articles
External references:
- https://tanstack.com/query/latest/docs/react/overview — TanStack Query v5 docs
- https://tanstack.com/router/latest/docs/framework/react/start — TanStack Router start guide
- https://tanstack.com/router/latest/docs/routing/code-based-routing — Code-based routing reference
- https://tanstack.com/table/latest/docs/react/overview — TanStack Table docs
- https://tailwindcss.com/docs/theme — Tailwind CSS v4 theme variables (
@themedirective)
Related KB articles (relative links):
- routing-patterns.md — File-based route naming, loader anatomy, nested layouts
- tailwind-v4-migration.md — Token migration from v3 config to v4
@theme - component-library-notes.md — Component naming, token usage, headless primitives
- website-rebuild.md — Rebuild context, tech choices, session history
- testing-setup.md — Route-level tests, component testing patterns
Change Log
2026-08-27 — Expanded to production format
- Added full format sections: Status, Last Updated, Category, Prerequisites, Tags, Summary, Context, Implementation, Examples, Pitfalls, Next Steps, Sources, Change Log.
- Expanded Query practices (keys, staleTime, select, meta, mutations, invalidation).
- Expanded Router practices (loaders, beforeLoad auth, typed navigation, route tree regeneration).
- Expanded Table adoption criteria and pagination patterns.
- Added three concrete implementation examples (loader, mutation, client isolation).
- Added Pitfalls table with 7 common errors and fixes.
- Added Next Steps aligning with dashboard and telemetry work.
- Updated external sources (TanStack Query v5, Router start, Table docs, Tailwind v4 theme docs).
- Cross-linked to related KB articles (routing-patterns, tailwind-v4-migration, component-library-notes, website-rebuild, testing-setup).
- Updated Status to Active, Last Updated to 2026-08-27.
2026-01-30 — Initial creation
- Converted TanStack guidance to KB template; documented query/router/table responsibilities.