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

TanStack Router — Routing Layer

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:


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


Sources & Related Articles

External references:

Related KB articles (relative links):


Change Log

2026-08-27 — Expanded to production format

2026-01-30 — Initial creation

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse