Files
dotfiles/dot_agents/skills/tanstack-integration-best-practices/rules/ssr-dehydrate-hydrate.md
T
Prad Nukala 6406208b35 shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables:
  ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone
  ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj
  (ghg-clone not named ghg: unrelated cargo binary owns that name)
- cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho
  cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files
- fish keeps only shell-state functions; converted ones now hit PATH
- cp-appid ignore gating moved to .local/bin path
- dot_agents/skills: 82 skills materialized from dotstate storage so
  dot_claude skill symlinks resolve on fresh bootstrap; dotstate
  profile/symlinks.json released .agents/skills .bashrc .zshrc
2026-08-10 15:15:06 -04:00

5.4 KiB

ssr-dehydrate-hydrate: Configure SSR Query Integration

Priority: CRITICAL

Explanation

Use @tanstack/react-router-ssr-query to automatically handle SSR dehydration/hydration between TanStack Router and TanStack Query. This package automates cache transfer, streaming, and redirect handling.

Bad Example

// Manual dehydration - verbose and error-prone
import { dehydrate, hydrate } from '@tanstack/react-query'

const router = createRouter({
  routeTree,
  context: { queryClient },

  // Manual approach - lots of boilerplate
  dehydrate: () => ({
    queryClientState: dehydrate(queryClient),
  }),

  hydrate: (dehydrated) => {
    hydrate(queryClient, dehydrated.queryClientState)
  },

  Wrap: ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  ),
})

Good Example: Modern SSR Integration

// router.tsx
import { QueryClient } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { routeTree } from './routeTree.gen'

export function getRouter() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        refetchOnWindowFocus: false,
        staleTime: 1000 * 60 * 2, // 2 minutes
      },
    },
  })

  const router = createRouter({
    routeTree,
    context: { queryClient },
    defaultPreload: 'intent',
    defaultPreloadStaleTime: 0, // Let Query manage cache freshness
    scrollRestoration: true,
    defaultStructuralSharing: true,
  })

  // Automatic SSR dehydration/hydration
  setupRouterSsrQueryIntegration({
    router,
    queryClient,
    handleRedirects: true,  // Intercept redirects from queries/mutations
    wrapQueryClient: true,  // Auto-wrap with QueryClientProvider
  })

  return router
}

Good Example: With Error and NotFound Components

import { DefaultCatchBoundary } from '@/components/DefaultCatchBoundary'
import { DefaultNotFound } from '@/components/DefaultNotFound'

export function getRouter() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        refetchOnWindowFocus: false,
        staleTime: 1000 * 60 * 2,
      },
    },
  })

  const router = createRouter({
    routeTree,
    context: { queryClient, user: null },
    defaultPreload: 'intent',
    defaultPreloadStaleTime: 0,
    defaultErrorComponent: DefaultCatchBoundary,
    defaultNotFoundComponent: DefaultNotFound,
    scrollRestoration: true,
    defaultStructuralSharing: true,
  })

  setupRouterSsrQueryIntegration({
    router,
    queryClient,
    handleRedirects: true,
    wrapQueryClient: true,
  })

  return router
}

Good Example: Custom QueryClientProvider

// If you need custom provider setup (e.g., for DevTools)
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

export function getRouter() {
  const queryClient = new QueryClient()

  const router = createRouter({
    routeTree,
    context: { queryClient },
    defaultPreload: 'intent',
    defaultPreloadStaleTime: 0,
    scrollRestoration: true,
  })

  setupRouterSsrQueryIntegration({
    router,
    queryClient,
    handleRedirects: true,
    wrapQueryClient: false, // We'll provide our own
  })

  // Custom wrapper with DevTools
  router.options.Wrap = ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
      {process.env.NODE_ENV === 'development' && (
        <ReactQueryDevtools initialIsOpen={false} />
      )}
    </QueryClientProvider>
  )

  return router
}

Good Example: Vite Configuration

// vite.config.ts
import { tanstackStart } from "@tanstack/start/plugin/vite"
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"

export default defineConfig({
  plugins: [
    tanstackStart(),  // Handles SSR entry points automatically
    react(),
  ],
})

TanStack Start handles client hydration and SSR automatically via the Vite plugin. No separate client.tsx or ssr.tsx files are needed.

setupRouterSsrQueryIntegration Options

Option Type Default Description
router Router Required Your router instance
queryClient QueryClient Required Your QueryClient instance
handleRedirects boolean true Intercept and handle redirects from queries/mutations
wrapQueryClient boolean true Wrap router with QueryClientProvider automatically

SSR Data Flow

Server:
  1. Request received
  2. getRouter() creates fresh QueryClient + Router
  3. setupRouterSsrQueryIntegration connects them
  4. Router matches routes, runs loaders
  5. Loaders call ensureQueryData → data cached
  6. Integration auto-dehydrates QueryClient state
  7. HTML + serialized state streamed to client

Client:
  1. HTML rendered (React hydrates)
  2. getRouter() creates fresh QueryClient + Router
  3. Integration auto-hydrates state from server
  4. useSuspenseQuery finds data in cache - no refetch!
  5. App is interactive with data already loaded

Context

  • Install: npm install @tanstack/react-router-ssr-query
  • Creates fresh QueryClient per request (required for SSR)
  • Handles streaming of queries that resolve during render
  • Set defaultPreloadStaleTime: 0 to let Query manage freshness
  • Each SSR request needs its own router instance via getRouter()
  • The integration handles all dehydration/hydration automatically