mirror of
https://github.com/DeckardGer/tanstack-agent-skills.git
synced 2026-09-14 20:46:35 +08:00
Update integration rules to modern TanStack Start patterns
- Replace manual dehydrate/hydrate with setupRouterSsrQueryIntegration - Remove outdated client.tsx/ssr.tsx entry file examples - Add tanstackStart() Vite plugin as entry point handler - Add router-default-options rule (scrollRestoration, defaultErrorComponent, etc.) - Update all examples to use getRouter() pattern - Change SSR integration priority from LOW to CRITICAL Based on react-tanstarter reference implementation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,9 +20,10 @@ Guidelines for integrating TanStack Query, Router, and Start together effectivel
|
||||
| Priority | Category | Rules | Impact |
|
||||
|----------|----------|-------|--------|
|
||||
| CRITICAL | Setup | 3 rules | Foundational configuration |
|
||||
| CRITICAL | SSR Integration | 1 rule | Router + Query SSR setup |
|
||||
| HIGH | Data Flow | 4 rules | Correct data fetching patterns |
|
||||
| MEDIUM | Caching | 3 rules | Performance optimization |
|
||||
| LOW | SSR | 3 rules | Server rendering patterns |
|
||||
| MEDIUM | SSR | 2 rules | Additional SSR patterns |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
@@ -45,9 +46,12 @@ Guidelines for integrating TanStack Query, Router, and Start together effectivel
|
||||
- `cache-preload-coordination` — Coordinate preloading between router and query
|
||||
- `cache-invalidation-patterns` — Unified invalidation patterns
|
||||
|
||||
### SSR (Prefix: `ssr-`)
|
||||
### SSR Integration (Prefix: `ssr-`)
|
||||
|
||||
- `ssr-dehydrate-hydrate` — Use setupRouterSsrQueryIntegration for automatic SSR
|
||||
|
||||
### Additional SSR (Prefix: `ssr-`)
|
||||
|
||||
- `ssr-dehydrate-hydrate` — Configure dehydration/hydration
|
||||
- `ssr-per-request-client` — Create QueryClient per request
|
||||
- `ssr-streaming-queries` — Handle streaming with queries
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## Explanation
|
||||
|
||||
When using TanStack Router with TanStack Query, let Query be the single source of truth for caching. Disable or minimize Router's built-in cache to avoid confusion about which cache is authoritative.
|
||||
When using TanStack Router with TanStack Query, let Query be the single source of truth for caching. Disable Router's built-in cache with `defaultPreloadStaleTime: 0` to avoid confusion about which cache is authoritative.
|
||||
|
||||
## Bad Example
|
||||
|
||||
@@ -41,21 +41,36 @@ function PostsPage() {
|
||||
|
||||
```tsx
|
||||
// router.tsx - Disable router cache when using Query
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
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'
|
||||
|
||||
// Let Query manage caching
|
||||
defaultPreloadStaleTime: 0, // Router doesn't cache
|
||||
export function getRouter() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// SSR integration
|
||||
dehydrate: () => ({
|
||||
queryClientState: dehydrate(queryClient),
|
||||
}),
|
||||
hydrate: (dehydrated) => {
|
||||
hydrate(queryClient, dehydrated.queryClientState)
|
||||
},
|
||||
})
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
defaultPreload: 'intent',
|
||||
defaultPreloadStaleTime: 0, // Let Query manage caching
|
||||
scrollRestoration: true,
|
||||
})
|
||||
|
||||
setupRouterSsrQueryIntegration({
|
||||
router,
|
||||
queryClient,
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// routes/posts.tsx
|
||||
export const Route = createFileRoute('/posts')({
|
||||
@@ -84,14 +99,14 @@ function PostsPage() {
|
||||
| Optimistic updates | No | Yes |
|
||||
| Mutations | No built-in | Full support |
|
||||
| DevTools | Limited | Rich debugging |
|
||||
| Cross-route sharing | Limited | Full |
|
||||
| Cross-route sharing | Full | Full |
|
||||
|
||||
## Good Example: Coordinated Caching Config
|
||||
|
||||
```tsx
|
||||
// lib/query-client.ts
|
||||
export function createQueryClient() {
|
||||
return new QueryClient({
|
||||
// router.tsx
|
||||
export function getRouter() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000, // Fresh for 1 minute
|
||||
@@ -101,25 +116,22 @@ export function createQueryClient() {
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// router.tsx
|
||||
export function createAppRouter() {
|
||||
const queryClient = createQueryClient()
|
||||
|
||||
return createRouter({
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
|
||||
// Router defers to Query for all caching decisions
|
||||
defaultPreloadStaleTime: 0,
|
||||
|
||||
Wrap: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
defaultPreload: 'intent',
|
||||
defaultPreloadStaleTime: 0, // Router defers to Query
|
||||
scrollRestoration: true,
|
||||
defaultStructuralSharing: true,
|
||||
})
|
||||
|
||||
setupRouterSsrQueryIntegration({
|
||||
router,
|
||||
queryClient,
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
@@ -127,12 +139,20 @@ export function createAppRouter() {
|
||||
|
||||
```tsx
|
||||
// Preloading still works - it just uses Query's cache
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
defaultPreload: 'intent', // Preload on hover
|
||||
defaultPreloadStaleTime: 0, // Query decides if data is stale
|
||||
})
|
||||
export function getRouter() {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
defaultPreload: 'intent', // Preload on hover
|
||||
defaultPreloadStaleTime: 0, // Query decides if data is stale
|
||||
})
|
||||
|
||||
setupRouterSsrQueryIntegration({ router, queryClient })
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// When user hovers a Link:
|
||||
// 1. Router triggers preload
|
||||
@@ -164,4 +184,4 @@ const createPost = useMutation({
|
||||
- Preloading still works - just uses Query's cache
|
||||
- Mutations, optimistic updates, invalidation all work normally
|
||||
- DevTools show the single authoritative cache state
|
||||
- SSR hydration uses Query's dehydrate/hydrate
|
||||
- Use `setupRouterSsrQueryIntegration` for SSR hydration
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## Explanation
|
||||
|
||||
Pass the QueryClient instance through TanStack Router's context system rather than using a global. This enables proper SSR with per-request clients, testability, and type-safe access in loaders.
|
||||
Pass the QueryClient instance through TanStack Router's context system rather than using a global. This enables proper SSR with per-request clients, testability, and type-safe access in loaders. Use `@tanstack/react-router-ssr-query` for automatic SSR integration.
|
||||
|
||||
## Bad Example
|
||||
|
||||
@@ -23,7 +23,7 @@ export const Route = createFileRoute('/posts')({
|
||||
})
|
||||
```
|
||||
|
||||
## Good Example
|
||||
## Good Example: Modern Router Setup
|
||||
|
||||
```tsx
|
||||
// routes/__root.tsx
|
||||
@@ -39,30 +39,32 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
})
|
||||
|
||||
// router.tsx
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { createRouter } from '@tanstack/react-router'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
export function createAppRouter() {
|
||||
export function getRouter() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {
|
||||
queryClient,
|
||||
},
|
||||
// Wrap entire app with QueryClientProvider
|
||||
Wrap: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
context: { queryClient },
|
||||
defaultPreload: 'intent',
|
||||
defaultPreloadStaleTime: 0,
|
||||
scrollRestoration: true,
|
||||
})
|
||||
|
||||
setupRouterSsrQueryIntegration({
|
||||
router,
|
||||
queryClient,
|
||||
})
|
||||
|
||||
return router
|
||||
@@ -70,7 +72,7 @@ export function createAppRouter() {
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: ReturnType<typeof createAppRouter>
|
||||
router: ReturnType<typeof getRouter>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,37 +85,43 @@ export const Route = createFileRoute('/posts')({
|
||||
})
|
||||
```
|
||||
|
||||
## Good Example: SSR with Per-Request Client
|
||||
## Good Example: Root Route with Context
|
||||
|
||||
```tsx
|
||||
// entry-server.tsx
|
||||
import { createAppRouter } from './router'
|
||||
// routes/__root.tsx
|
||||
import { createRootRouteWithContext, Outlet, HeadContent, Scripts } from '@tanstack/react-router'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
export async function render(req: Request) {
|
||||
// Create fresh QueryClient for each request
|
||||
const router = createAppRouter()
|
||||
|
||||
// Wait for critical data to load
|
||||
await router.load()
|
||||
|
||||
const html = renderToString(
|
||||
<RouterProvider router={router} />
|
||||
)
|
||||
|
||||
return html
|
||||
interface RouterContext {
|
||||
queryClient: QueryClient
|
||||
user: User | null
|
||||
}
|
||||
|
||||
// entry-client.tsx
|
||||
import { createAppRouter } from './router'
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
component: RootComponent,
|
||||
beforeLoad: async ({ context }) => {
|
||||
// Prefetch auth or other global data
|
||||
await context.queryClient.ensureQueryData(authQueryOptions)
|
||||
},
|
||||
})
|
||||
|
||||
const router = createAppRouter()
|
||||
|
||||
hydrateRoot(
|
||||
document.getElementById('app')!,
|
||||
<RouterProvider router={router} />
|
||||
)
|
||||
function RootComponent() {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<Outlet />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
TanStack Start handles SSR and hydration automatically via the Vite plugin. No separate entry files needed.
|
||||
|
||||
## Good Example: Testing with Mock QueryClient
|
||||
|
||||
```tsx
|
||||
@@ -159,6 +167,7 @@ test('loads posts', async () => {
|
||||
|
||||
- Router context flows to all loaders and beforeLoad hooks
|
||||
- Creating QueryClient per request is essential for SSR
|
||||
- Use `Wrap` option for provider wrapping
|
||||
- Use `setupRouterSsrQueryIntegration` for automatic SSR handling
|
||||
- Access queryClient via `context` parameter in loaders
|
||||
- This pattern enables clean dependency injection for testing
|
||||
- Install: `npm install @tanstack/react-router-ssr-query`
|
||||
|
||||
@@ -1,204 +1,206 @@
|
||||
# ssr-dehydrate-hydrate: Configure Dehydration/Hydration
|
||||
# ssr-dehydrate-hydrate: Configure SSR Query Integration
|
||||
|
||||
## Priority: LOW
|
||||
## Priority: CRITICAL
|
||||
|
||||
## Explanation
|
||||
|
||||
For SSR with TanStack Start, configure the router to dehydrate Query's cache on the server and hydrate it on the client. This transfers prefetched data to the client, preventing duplicate requests.
|
||||
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
|
||||
|
||||
```tsx
|
||||
// No SSR configuration - data refetches on client
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
// Missing dehydrate/hydrate configuration
|
||||
})
|
||||
// Manual dehydration - verbose and error-prone
|
||||
import { dehydrate, hydrate } from '@tanstack/react-query'
|
||||
|
||||
// Server prefetches data
|
||||
export const Route = createFileRoute('/posts')({
|
||||
loader: async ({ context: { queryClient } }) => {
|
||||
await queryClient.ensureQueryData(postQueries.all())
|
||||
},
|
||||
})
|
||||
|
||||
// Client doesn't receive prefetched data
|
||||
// useSuspenseQuery refetches on mount
|
||||
```
|
||||
|
||||
## Good Example: Full SSR Configuration
|
||||
|
||||
```tsx
|
||||
// router.tsx
|
||||
import { createRouter } from '@tanstack/react-router'
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
dehydrate,
|
||||
hydrate,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
export function createAppRouter() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
// Higher gcTime on server to survive serialization
|
||||
gcTime: Infinity,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
|
||||
// Disable router caching - Query handles it
|
||||
defaultPreloadStaleTime: 0,
|
||||
|
||||
// SSR: Serialize Query cache to send to client
|
||||
dehydrate: () => ({
|
||||
queryClientState: dehydrate(queryClient, {
|
||||
shouldDehydrateQuery: (query) => {
|
||||
// Only dehydrate successful queries
|
||||
return query.state.status === 'success'
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
// SSR: Restore Query cache on client
|
||||
hydrate: (dehydrated) => {
|
||||
hydrate(queryClient, dehydrated.queryClientState)
|
||||
},
|
||||
|
||||
Wrap: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example: TanStack Start Entry Files
|
||||
|
||||
```tsx
|
||||
// app/ssr.tsx
|
||||
import { createRouter } from './router'
|
||||
import { getRouterManifest } from '@tanstack/react-start/router-manifest'
|
||||
import {
|
||||
createStartHandler,
|
||||
defaultStreamHandler,
|
||||
} from '@tanstack/react-start/server'
|
||||
|
||||
export default createStartHandler({
|
||||
createRouter,
|
||||
getRouterManifest,
|
||||
})(defaultStreamHandler)
|
||||
|
||||
// app/client.tsx
|
||||
import { createRouter } from './router'
|
||||
import { StartClient } from '@tanstack/react-start'
|
||||
import { hydrateRoot } from 'react-dom/client'
|
||||
|
||||
const router = createRouter()
|
||||
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StartClient router={router} />
|
||||
)
|
||||
```
|
||||
|
||||
## Good Example: Selective Dehydration
|
||||
|
||||
```tsx
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
|
||||
// Manual approach - lots of boilerplate
|
||||
dehydrate: () => ({
|
||||
queryClientState: dehydrate(queryClient, {
|
||||
shouldDehydrateQuery: (query) => {
|
||||
// Don't dehydrate failed queries
|
||||
if (query.state.status !== 'success') return false
|
||||
|
||||
// Don't dehydrate user-specific data in shared cache
|
||||
if (query.queryKey[0] === 'user-private') return false
|
||||
|
||||
// Don't dehydrate large payloads
|
||||
if (query.state.dataUpdateCount > 0) {
|
||||
const dataSize = JSON.stringify(query.state.data).length
|
||||
if (dataSize > 100_000) return false // Skip if > 100KB
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
}),
|
||||
queryClientState: dehydrate(queryClient),
|
||||
}),
|
||||
|
||||
hydrate: (dehydrated) => {
|
||||
hydrate(queryClient, dehydrated.queryClientState)
|
||||
},
|
||||
|
||||
Wrap: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
## Good Example: With React Query DevTools
|
||||
## Good Example: Modern SSR Integration
|
||||
|
||||
```tsx
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
// 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 createAppRouter() {
|
||||
const queryClient = new QueryClient(/* ... */)
|
||||
export function getRouter() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return createRouter({
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
|
||||
dehydrate: () => ({
|
||||
queryClientState: dehydrate(queryClient),
|
||||
}),
|
||||
|
||||
hydrate: (dehydrated) => {
|
||||
hydrate(queryClient, dehydrated.queryClientState)
|
||||
},
|
||||
|
||||
Wrap: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
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
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
```tsx
|
||||
// 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
|
||||
|
||||
```ts
|
||||
// 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. createRouter() creates fresh QueryClient
|
||||
3. Router matches routes, runs loaders
|
||||
4. Loaders call ensureQueryData → data cached in QueryClient
|
||||
5. dehydrate() serializes QueryClient state
|
||||
6. HTML + serialized state sent to client
|
||||
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. createRouter() creates fresh QueryClient
|
||||
3. hydrate() restores state from server
|
||||
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
|
||||
|
||||
- `dehydrate()` extracts serializable state from QueryClient
|
||||
- `hydrate()` restores state into a QueryClient
|
||||
- Only successful queries are dehydrated by default
|
||||
- Set `staleTime > 0` to prevent immediate client refetch
|
||||
- Each SSR request needs its own QueryClient instance
|
||||
- DevTools only show in development builds
|
||||
- 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
|
||||
|
||||
@@ -23,6 +23,7 @@ Comprehensive guidelines for implementing TanStack Router patterns in React appl
|
||||
|----------|----------|-------|--------|
|
||||
| CRITICAL | Type Safety | 4 rules | Prevents runtime errors and enables refactoring |
|
||||
| CRITICAL | Route Organization | 5 rules | Ensures maintainable route structure |
|
||||
| HIGH | Router Config | 1 rule | Global router defaults |
|
||||
| HIGH | Data Loading | 6 rules | Optimizes data fetching and caching |
|
||||
| HIGH | Search Params | 5 rules | Enables type-safe URL state |
|
||||
| HIGH | Error Handling | 1 rule | Handles 404 and errors gracefully |
|
||||
@@ -40,6 +41,10 @@ Comprehensive guidelines for implementing TanStack Router patterns in React appl
|
||||
- `ts-route-context-typing` — Type route context with createRootRouteWithContext
|
||||
- `ts-query-options-loader` — Use queryOptions in loaders for type inference
|
||||
|
||||
### Router Config (Prefix: `router-`)
|
||||
|
||||
- `router-default-options` — Configure router defaults (scrollRestoration, defaultErrorComponent, etc.)
|
||||
|
||||
### Route Organization (Prefix: `org-`)
|
||||
|
||||
- `org-file-based-routing` — Prefer file-based routing for conventions
|
||||
|
||||
@@ -63,19 +63,27 @@ function RootComponent() {
|
||||
|
||||
// router.tsx - Provide context when creating router
|
||||
import { createRouter } from '@tanstack/react-router'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
export function getRouter(auth: RouterContext['auth'] = { user: null, isAuthenticated: false }) {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
export function createAppRouter(auth: RouterContext['auth']) {
|
||||
return createRouter({
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {
|
||||
queryClient,
|
||||
auth,
|
||||
},
|
||||
defaultPreload: 'intent',
|
||||
defaultPreloadStaleTime: 0,
|
||||
scrollRestoration: true,
|
||||
})
|
||||
|
||||
setupRouterSsrQueryIntegration({ router, queryClient })
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// routes/posts.tsx - Use context in loaders
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# router-default-options: Configure Router Default Options
|
||||
|
||||
## Priority: HIGH
|
||||
|
||||
## Explanation
|
||||
|
||||
TanStack Router's `createRouter` accepts several default options that apply globally. Configure these for consistent behavior across your application including error handling, scroll restoration, and performance optimizations.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```tsx
|
||||
// Minimal router - missing useful defaults
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
})
|
||||
|
||||
// Each route must handle its own errors
|
||||
// No scroll restoration on navigation
|
||||
// No preloading configured
|
||||
```
|
||||
|
||||
## Good Example: Full Configuration
|
||||
|
||||
```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'
|
||||
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 },
|
||||
|
||||
// Preloading
|
||||
defaultPreload: 'intent', // Preload on hover/focus
|
||||
defaultPreloadStaleTime: 0, // Let Query manage freshness
|
||||
|
||||
// Error handling
|
||||
defaultErrorComponent: DefaultCatchBoundary,
|
||||
defaultNotFoundComponent: DefaultNotFound,
|
||||
|
||||
// UX
|
||||
scrollRestoration: true, // Restore scroll on back/forward
|
||||
|
||||
// Performance
|
||||
defaultStructuralSharing: true, // Optimize re-renders
|
||||
})
|
||||
|
||||
setupRouterSsrQueryIntegration({
|
||||
router,
|
||||
queryClient,
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example: DefaultCatchBoundary Component
|
||||
|
||||
```tsx
|
||||
// components/DefaultCatchBoundary.tsx
|
||||
import { ErrorComponent, useRouter } from '@tanstack/react-router'
|
||||
|
||||
export function DefaultCatchBoundary({ error }: { error: Error }) {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className="error-container">
|
||||
<h1>Something went wrong</h1>
|
||||
<ErrorComponent error={error} />
|
||||
<button onClick={() => router.invalidate()}>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example: DefaultNotFound Component
|
||||
|
||||
```tsx
|
||||
// components/DefaultNotFound.tsx
|
||||
import { Link } from '@tanstack/react-router'
|
||||
|
||||
export function DefaultNotFound() {
|
||||
return (
|
||||
<div className="not-found-container">
|
||||
<h1>404 - Page Not Found</h1>
|
||||
<p>The page you're looking for doesn't exist.</p>
|
||||
<Link to="/">Go home</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Router Options Reference
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `defaultPreload` | `false \| 'intent' \| 'render' \| 'viewport'` | `false` | When to preload routes |
|
||||
| `defaultPreloadStaleTime` | `number` | `30000` | How long preloaded data stays fresh (ms) |
|
||||
| `defaultErrorComponent` | `Component` | Built-in | Global error boundary |
|
||||
| `defaultNotFoundComponent` | `Component` | Built-in | Global 404 page |
|
||||
| `scrollRestoration` | `boolean` | `false` | Restore scroll on navigation |
|
||||
| `defaultStructuralSharing` | `boolean` | `true` | Optimize loader data re-renders |
|
||||
|
||||
## Good Example: Route-Level Overrides
|
||||
|
||||
```tsx
|
||||
// Routes can override defaults
|
||||
export const Route = createFileRoute('/admin')({
|
||||
// Custom error handling for admin section
|
||||
errorComponent: AdminErrorBoundary,
|
||||
notFoundComponent: AdminNotFound,
|
||||
|
||||
// Disable preload for sensitive routes
|
||||
preload: false,
|
||||
})
|
||||
```
|
||||
|
||||
## Good Example: With Pending Component
|
||||
|
||||
```tsx
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
|
||||
defaultPreload: 'intent',
|
||||
defaultPreloadStaleTime: 0,
|
||||
defaultErrorComponent: DefaultCatchBoundary,
|
||||
defaultNotFoundComponent: DefaultNotFound,
|
||||
scrollRestoration: true,
|
||||
|
||||
// Show during route transitions
|
||||
defaultPendingComponent: () => (
|
||||
<div className="loading-bar" />
|
||||
),
|
||||
defaultPendingMinMs: 200, // Min time to show pending UI
|
||||
defaultPendingMs: 1000, // Delay before showing pending UI
|
||||
})
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
- Set `defaultPreloadStaleTime: 0` when using TanStack Query
|
||||
- `scrollRestoration: true` improves back/forward navigation UX
|
||||
- `defaultStructuralSharing` prevents unnecessary re-renders
|
||||
- Route-level options override router defaults
|
||||
- Error/NotFound components receive route context
|
||||
- Pending components help with perceived performance
|
||||
Reference in New Issue
Block a user