mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-02 19:32:43 +00:00
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
This commit is contained in:
1 parent
6776366ce6
commit
6406208b35
637 files changed
+100297
-497
No files matched your search
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: tanstack-integration-best-practices
|
||||
description: Best practices for integrating TanStack Query with TanStack Router and TanStack Start. Patterns for full-stack data flow, SSR, and caching coordination.
|
||||
---
|
||||
|
||||
# TanStack Integration Best Practices
|
||||
|
||||
Guidelines for integrating TanStack Query, Router, and Start together effectively. These patterns ensure optimal data flow, caching coordination, and type safety across the stack.
|
||||
|
||||
## When to Apply
|
||||
|
||||
- Setting up a new TanStack Start project
|
||||
- Integrating TanStack Query with TanStack Router
|
||||
- Configuring SSR with query hydration
|
||||
- Coordinating caching between router and query
|
||||
- Setting up type-safe data fetching patterns
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| 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 |
|
||||
| MEDIUM | SSR | 2 rules | Additional SSR patterns |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Setup (Prefix: `setup-`)
|
||||
|
||||
- `setup-query-client-context` — Pass QueryClient through router context
|
||||
- `setup-provider-wrapping` — Correctly wrap with QueryClientProvider
|
||||
- `setup-stale-time-coordination` — Coordinate staleTime between router and query
|
||||
|
||||
### Data Flow (Prefix: `flow-`)
|
||||
|
||||
- `flow-loader-query-pattern` — Use loaders with ensureQueryData
|
||||
- `flow-suspense-query-component` — Use useSuspenseQuery in components
|
||||
- `flow-mutations-invalidation` — Coordinate mutations with query invalidation
|
||||
- `flow-server-functions-queries` — Use server functions for query functions
|
||||
|
||||
### Caching (Prefix: `cache-`)
|
||||
|
||||
- `cache-single-source` — Let TanStack Query manage caching
|
||||
- `cache-preload-coordination` — Coordinate preloading between router and query
|
||||
- `cache-invalidation-patterns` — Unified invalidation patterns
|
||||
|
||||
### SSR Integration (Prefix: `ssr-`)
|
||||
|
||||
- `ssr-dehydrate-hydrate` — Use setupRouterSsrQueryIntegration for automatic SSR
|
||||
|
||||
### Additional SSR (Prefix: `ssr-`)
|
||||
|
||||
- `ssr-per-request-client` — Create QueryClient per request
|
||||
- `ssr-streaming-queries` — Handle streaming with queries
|
||||
|
||||
## How to Use
|
||||
|
||||
Each rule file in the `rules/` directory contains:
|
||||
1. **Explanation** — Why this pattern matters
|
||||
2. **Bad Example** — Anti-pattern to avoid
|
||||
3. **Good Example** — Recommended implementation
|
||||
4. **Context** — When to apply or skip this rule
|
||||
|
||||
## Full Reference
|
||||
|
||||
See individual rule files in `rules/` directory for detailed guidance and code examples.
|
||||
@@ -0,0 +1,187 @@
|
||||
# cache-single-source: Let TanStack Query Manage Caching
|
||||
|
||||
## Priority: MEDIUM
|
||||
|
||||
## Explanation
|
||||
|
||||
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
|
||||
|
||||
```tsx
|
||||
// Both Router and Query caching enabled - confusing
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
// Default router caching enabled
|
||||
// defaultPreloadStaleTime: 30000 (default)
|
||||
})
|
||||
|
||||
export const Route = createFileRoute('/posts')({
|
||||
loader: async () => {
|
||||
// Fetches directly - cached by Router
|
||||
const posts = await fetchPosts()
|
||||
return { posts }
|
||||
},
|
||||
component: PostsPage,
|
||||
})
|
||||
|
||||
function PostsPage() {
|
||||
// Also uses Query cache - which is authoritative?
|
||||
const { data } = useQuery({
|
||||
queryKey: ['posts'],
|
||||
queryFn: fetchPosts,
|
||||
})
|
||||
|
||||
// Now there are TWO caches with potentially different data
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```tsx
|
||||
// router.tsx - Disable router cache when using Query
|
||||
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: {
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
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')({
|
||||
loader: async ({ context: { queryClient } }) => {
|
||||
// Query is the single cache source
|
||||
await queryClient.ensureQueryData(postQueries.all())
|
||||
// No return needed - data lives in Query cache
|
||||
},
|
||||
component: PostsPage,
|
||||
})
|
||||
|
||||
function PostsPage() {
|
||||
// Single source of truth
|
||||
const { data: posts } = useSuspenseQuery(postQueries.all())
|
||||
|
||||
return <PostList posts={posts} />
|
||||
}
|
||||
```
|
||||
|
||||
## Cache Comparison
|
||||
|
||||
| Feature | Router Cache | Query Cache |
|
||||
|---------|-------------|-------------|
|
||||
| Invalidation | Manual/time-based | Query keys, patterns |
|
||||
| Background refetch | No | Yes |
|
||||
| Optimistic updates | No | Yes |
|
||||
| Mutations | No built-in | Full support |
|
||||
| DevTools | Limited | Rich debugging |
|
||||
| Cross-route sharing | Full | Full |
|
||||
|
||||
## Good Example: Coordinated Caching Config
|
||||
|
||||
```tsx
|
||||
// router.tsx
|
||||
export function getRouter() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000, // Fresh for 1 minute
|
||||
gcTime: 10 * 60 * 1000, // Cache for 10 minutes
|
||||
refetchOnWindowFocus: true, // Refetch when tab focused
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
defaultPreload: 'intent',
|
||||
defaultPreloadStaleTime: 0, // Router defers to Query
|
||||
scrollRestoration: true,
|
||||
defaultStructuralSharing: true,
|
||||
})
|
||||
|
||||
setupRouterSsrQueryIntegration({
|
||||
router,
|
||||
queryClient,
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example: Preload Still Works
|
||||
|
||||
```tsx
|
||||
// Preloading still works - it just uses Query's cache
|
||||
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
|
||||
// 2. Loader runs ensureQueryData
|
||||
// 3. Query checks its cache - fresh? skip fetch. stale? refetch.
|
||||
// 4. User clicks - data already in Query cache
|
||||
```
|
||||
|
||||
## Mutation Invalidation
|
||||
|
||||
```tsx
|
||||
// Mutations properly invalidate the single cache
|
||||
const createPost = useMutation({
|
||||
mutationFn: submitPost,
|
||||
onSuccess: () => {
|
||||
// Invalidate Query cache - the single source
|
||||
queryClient.invalidateQueries({ queryKey: ['posts'] })
|
||||
|
||||
// Router automatically uses updated cache on next navigation
|
||||
navigate({ to: '/posts' })
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
- `defaultPreloadStaleTime: 0` means "always ask Query"
|
||||
- Query's staleTime/gcTime controls caching behavior
|
||||
- Preloading still works - just uses Query's cache
|
||||
- Mutations, optimistic updates, invalidation all work normally
|
||||
- DevTools show the single authoritative cache state
|
||||
- Use `setupRouterSsrQueryIntegration` for SSR hydration
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
# flow-loader-query-pattern: Use Loaders with ensureQueryData
|
||||
|
||||
## Priority: HIGH
|
||||
|
||||
## Explanation
|
||||
|
||||
The recommended pattern combines TanStack Router's loaders with TanStack Query's `ensureQueryData`. Loaders prefetch data during navigation, while Query manages caching and updates. Use `useSuspenseQuery` in components since data is guaranteed.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```tsx
|
||||
// Only using Query in component - loading waterfall
|
||||
function PostsPage() {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['posts'],
|
||||
queryFn: fetchPosts,
|
||||
})
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
return <PostList posts={data} />
|
||||
}
|
||||
|
||||
// Only using loader - no cache management
|
||||
export const Route = createFileRoute('/posts')({
|
||||
loader: async () => {
|
||||
const posts = await fetchPosts() // Not cached
|
||||
return { posts }
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```tsx
|
||||
// lib/queries/posts.ts - Define queryOptions
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
|
||||
export const postQueries = {
|
||||
all: () => queryOptions({
|
||||
queryKey: ['posts'],
|
||||
queryFn: fetchPosts,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
}),
|
||||
detail: (id: string) => queryOptions({
|
||||
queryKey: ['posts', id],
|
||||
queryFn: () => fetchPost(id),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
}),
|
||||
}
|
||||
|
||||
// routes/posts.tsx
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { postQueries } from '@/lib/queries/posts'
|
||||
|
||||
export const Route = createFileRoute('/posts')({
|
||||
loader: async ({ context: { queryClient } }) => {
|
||||
// Prefetch in loader - runs during navigation
|
||||
await queryClient.ensureQueryData(postQueries.all())
|
||||
},
|
||||
component: PostsPage,
|
||||
})
|
||||
|
||||
function PostsPage() {
|
||||
// Data guaranteed by loader - no loading state needed
|
||||
const { data: posts } = useSuspenseQuery(postQueries.all())
|
||||
|
||||
return <PostList posts={posts} />
|
||||
}
|
||||
|
||||
// routes/posts/$postId.tsx
|
||||
export const Route = createFileRoute('/posts/$postId')({
|
||||
loader: async ({ params, context: { queryClient } }) => {
|
||||
await queryClient.ensureQueryData(postQueries.detail(params.postId))
|
||||
},
|
||||
component: PostDetailPage,
|
||||
})
|
||||
|
||||
function PostDetailPage() {
|
||||
const { postId } = Route.useParams()
|
||||
const { data: post } = useSuspenseQuery(postQueries.detail(postId))
|
||||
|
||||
return <PostContent post={post} />
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example: Parallel Data Loading
|
||||
|
||||
```tsx
|
||||
export const Route = createFileRoute('/dashboard')({
|
||||
loader: async ({ context: { queryClient } }) => {
|
||||
// Load multiple queries in parallel
|
||||
await Promise.all([
|
||||
queryClient.ensureQueryData(statsQueries.overview()),
|
||||
queryClient.ensureQueryData(activityQueries.recent()),
|
||||
queryClient.ensureQueryData(userQueries.current()),
|
||||
])
|
||||
},
|
||||
component: DashboardPage,
|
||||
})
|
||||
|
||||
function DashboardPage() {
|
||||
// All data ready - no loading states
|
||||
const { data: stats } = useSuspenseQuery(statsQueries.overview())
|
||||
const { data: activity } = useSuspenseQuery(activityQueries.recent())
|
||||
const { data: user } = useSuspenseQuery(userQueries.current())
|
||||
|
||||
return (
|
||||
<Dashboard
|
||||
stats={stats}
|
||||
activity={activity}
|
||||
user={user}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example: Optional Prefetch with Non-Critical Data
|
||||
|
||||
```tsx
|
||||
export const Route = createFileRoute('/posts/$postId')({
|
||||
loader: async ({ params, context: { queryClient } }) => {
|
||||
// Critical data - await it
|
||||
await queryClient.ensureQueryData(postQueries.detail(params.postId))
|
||||
|
||||
// Non-critical - prefetch but don't await
|
||||
queryClient.prefetchQuery(postQueries.comments(params.postId))
|
||||
queryClient.prefetchQuery(postQueries.related(params.postId))
|
||||
},
|
||||
component: PostPage,
|
||||
})
|
||||
|
||||
function PostPage() {
|
||||
const { postId } = Route.useParams()
|
||||
|
||||
// Critical - guaranteed by loader
|
||||
const { data: post } = useSuspenseQuery(postQueries.detail(postId))
|
||||
|
||||
// Non-critical - may still be loading
|
||||
const { data: comments, isLoading: commentsLoading } = useQuery(
|
||||
postQueries.comments(postId)
|
||||
)
|
||||
|
||||
return (
|
||||
<article>
|
||||
<PostContent post={post} />
|
||||
{commentsLoading ? <CommentsSkeleton /> : <Comments data={comments} />}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Data Flow Summary
|
||||
|
||||
```
|
||||
Navigation Starts
|
||||
↓
|
||||
Router matches route
|
||||
↓
|
||||
loader() executes
|
||||
↓
|
||||
ensureQueryData() checks cache
|
||||
↓
|
||||
Fresh cache? → Return cached Stale/missing? → Fetch and cache
|
||||
↓ ↓
|
||||
Route renders Route renders
|
||||
↓ ↓
|
||||
useSuspenseQuery returns data useSuspenseQuery returns data
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
- `ensureQueryData` respects staleTime - won't refetch fresh data
|
||||
- `useSuspenseQuery` throws promise to Suspense if data missing
|
||||
- Loaders enable preloading on link hover
|
||||
- This pattern eliminates loading waterfalls
|
||||
- Use `useQuery` for non-critical data that can load after render
|
||||
- Query invalidation and background updates still work normally
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
# setup-query-client-context: Pass QueryClient Through Router Context
|
||||
|
||||
## Priority: CRITICAL
|
||||
|
||||
## 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. Use `@tanstack/react-router-ssr-query` for automatic SSR integration.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```tsx
|
||||
// lib/query-client.ts - Global singleton
|
||||
export const queryClient = new QueryClient()
|
||||
|
||||
// routes/posts.tsx - Importing global
|
||||
import { queryClient } from '@/lib/query-client'
|
||||
|
||||
export const Route = createFileRoute('/posts')({
|
||||
loader: async () => {
|
||||
// Using global - breaks SSR, harder to test
|
||||
return queryClient.fetchQuery(postQueries.list())
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Good Example: Modern Router Setup
|
||||
|
||||
```tsx
|
||||
// routes/__root.tsx
|
||||
import { createRootRouteWithContext } from '@tanstack/react-router'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
interface RouterContext {
|
||||
queryClient: QueryClient
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
component: RootComponent,
|
||||
})
|
||||
|
||||
// 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,
|
||||
scrollRestoration: true,
|
||||
})
|
||||
|
||||
setupRouterSsrQueryIntegration({
|
||||
router,
|
||||
queryClient,
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: ReturnType<typeof getRouter>
|
||||
}
|
||||
}
|
||||
|
||||
// routes/posts.tsx - Access from context
|
||||
export const Route = createFileRoute('/posts')({
|
||||
loader: async ({ context: { queryClient } }) => {
|
||||
// Type-safe access to queryClient from context
|
||||
await queryClient.ensureQueryData(postQueries.list())
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Good Example: Root Route with Context
|
||||
|
||||
```tsx
|
||||
// routes/__root.tsx
|
||||
import { createRootRouteWithContext, Outlet, HeadContent, Scripts } from '@tanstack/react-router'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
interface RouterContext {
|
||||
queryClient: QueryClient
|
||||
user: User | null
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
component: RootComponent,
|
||||
beforeLoad: async ({ context }) => {
|
||||
// Prefetch auth or other global data
|
||||
await context.queryClient.ensureQueryData(authQueryOptions)
|
||||
},
|
||||
})
|
||||
|
||||
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
|
||||
// tests/posts.test.tsx
|
||||
import { createRouter, RouterProvider } from '@tanstack/react-router'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
function renderWithProviders(route: string) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
})
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: { queryClient },
|
||||
Wrap: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
})
|
||||
|
||||
return {
|
||||
...render(<RouterProvider router={router} />),
|
||||
queryClient,
|
||||
}
|
||||
}
|
||||
|
||||
test('loads posts', async () => {
|
||||
const { queryClient } = renderWithProviders('/posts')
|
||||
|
||||
// Pre-populate cache for testing
|
||||
queryClient.setQueryData(['posts'], mockPosts)
|
||||
|
||||
// ... assertions
|
||||
})
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
- Router context flows to all loaders and beforeLoad hooks
|
||||
- Creating QueryClient per request is essential for SSR
|
||||
- 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`
|
||||
@@ -0,0 +1,206 @@
|
||||
# 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
|
||||
|
||||
```tsx
|
||||
// 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
|
||||
|
||||
```tsx
|
||||
// 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
|
||||
|
||||
```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. 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
|
||||
Reference in new issue
Block a user