)
}
```
## Common Mistakes
### 1. HIGH: Not setting `defaultPreloadStaleTime` to 0
Router has a built-in preload cache (default `staleTime` for preloads is 30s). This prevents Query from controlling data freshness during preloading.
```tsx
// WRONG — Router's preload cache serves stale data, Query never refetches
const router = createRouter({ routeTree })
// CORRECT — disable Router's preload cache, let Query manage freshness
const router = createRouter({
routeTree,
defaultPreloadStaleTime: 0,
})
```
### 2. HIGH: Creating QueryClient outside `createRouter` for SSR
A module-level singleton `QueryClient` is shared across all server requests, leaking user data between requests.
```tsx
// WRONG — shared across SSR requests
const queryClient = new QueryClient()
export function createAppRouter() {
return createRouter({
routeTree,
context: { queryClient },
})
}
// CORRECT — new QueryClient per createAppRouter call
export function createAppRouter() {
const queryClient = new QueryClient()
return createRouter({
routeTree,
context: { queryClient },
})
}
```
### 3. MEDIUM: Awaiting a fire-and-forget query in a loader blocks rendering
A query used for streaming should start in the background and handle errors with `catch(noop)`. Awaiting it blocks the navigation transition until the data resolves, defeating the purpose of streaming.
```tsx
import { noop } from '@tanstack/react-query'
// WRONG — blocks navigation, no streaming benefit
loader: async ({ context }) => {
await context.queryClient.query({ ...analyticsQueryOptions }).catch(noop)
}
// CORRECT — fire and forget for streaming, with handled rejection
loader: ({ context }) => {
void context.queryClient.query({ ...analyticsQueryOptions }).catch(noop)
}
// If you need to block (critical data), use a static query instead:
loader: ({ context }) => {
return context.queryClient.query({
...criticalQueryOptions,
staleTime: 'static',
})
}
```
### 4. HIGH: Missing double parentheses on `createRootRouteWithContext`
`createRootRouteWithContext()` is a factory — it returns a function. The second call passes route options.
```tsx
// WRONG — passing options to the factory, not the returned function
const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>({
component: RootComponent,
})
// CORRECT — double call: factory()({options})
const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>()({
component: RootComponent,
})
```
## Tension: Built-In SWR Cache vs External Cache
TanStack Router has its own SWR cache (`staleTime`, `gcTime`, `defaultPreloadStaleTime`). When using Query as an external cache:
- Set `defaultPreloadStaleTime: 0` to prevent Router's cache from short-circuiting Query's freshness logic
- Router's `staleTime`/`gcTime` still apply to the loader return value. For pure Query patterns, return nothing from the loader (just a static `query` for the side effect) and read data exclusively from `useSuspenseQuery`
- `router.invalidate()` re-runs loaders (which call `query` with `staleTime: 'static'`), so the cached Query data remains the freshness authority
## Cross-References
- [router-core/data-loading](../../../../router-core/skills/router-core/data-loading/SKILL.md) — built-in loader caching fundamentals
- [router-core/ssr](../../../../router-core/skills/router-core/ssr/SKILL.md) — SSR setup for dehydration/hydration