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:
Deckard Gerritsen
2026-01-16 16:51:21 +08:00
parent 9c57b2b509
commit 41718b4641
7 changed files with 455 additions and 244 deletions
+7 -3
View File
@@ -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
+5
View File
@@ -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