@tanstack/react-start
Version:
Modern and scalable routing for React applications
138 lines (84 loc) • 4.32 kB
Markdown
Use Router when the RSC is route-shaped and keyed by URL state.
Relevant facts:
- Router loader cache keys are based on pathname and params, plus anything you add through `loaderDeps`
- default navigation `staleTime` is `0`
- default preload freshness is `30s`
- default stale reload mode is background stale-while-revalidate
- `router.invalidate()` reloads active loaders immediately and marks cached routes stale
Use Router ownership when the fragment naturally belongs to the route and should track navigation.
Use Query when the fragment has its own lifecycle.
Typical reasons:
- reuse across routes
- background refetching independent of navigation
- manual query invalidation
- non-route ownership
Hard rule:
```tsx
structuralSharing: false
```
Without that, Query may try to merge RSC values across fetches.
Use GET server functions and response headers when you want cross-request reuse:
- CDN caching
- edge caching
- reverse proxies
- shared server caches
Set headers in a GET `createServerFn` via `setResponseHeaders(...)`.
Use a server function to return the RSC, then fetch it in the route loader.
Tune Router with:
- `staleTime` when revisits should stay fresh for a window
- `loaderDeps` when search params affect the fragment
- `staleReloadMode: 'blocking'` when showing stale data during reload is unacceptable
`loaderDeps` rule: include only values the loader actually uses. Returning the whole search object causes noisy invalidation.
Use Query when you want explicit cache keys and background fetch behavior.
Recommended shape:
```tsx
const postQueryOptions = (postId: string) => ({
queryKey: ['post-rsc', postId],
structuralSharing: false,
queryFn: () => getPostRsc({ data: { postId } }),
staleTime: 5 * 60 * 1000,
})
```
For SSR reuse, prefetch in the route loader:
```tsx
await context.queryClient.ensureQueryData(postQueryOptions(params.postId))
```
Then read it with `useSuspenseQuery` in the component.
- loader-owned fragment changed -> `router.invalidate()`
- query-owned fragment changed -> `queryClient.invalidateQueries(...)`
- shared route and query state -> refresh both if both are authoritative
- CDN or response-header cache involved -> make sure the mutation path also busts or bypasses server-side cache
Do not “spray” invalidation everywhere. Decide who owns freshness, then target that owner.
Use when:
- the route component needs browser APIs
- the loader can still fetch the RSC on the server for first paint
- you want server-fetched fragment data ready before the client component mounts
This is often the right shape for responsive charts, viewport-dependent layout wrappers, or widgets that need `window` but still benefit from server-rendered content.
Use when the loader itself depends on browser APIs such as:
- `localStorage`
- `window`
- browser-only client state
In this mode, both the loader and component run in the browser. The loader can still call a server function that returns an RSC.
If the fragments do not share data, fetch them with separate server functions and `Promise.all`.
If several fragments always share a fetch or invalidate together, return them from one server function. This reduces round trips and keeps ownership aligned.
Return promises from the loader instead of awaiting them. Resolve them with `use()` inside Suspense. This also lets you isolate failures with a local ErrorBoundary instead of the route error boundary.
Use async generators when items should arrive incrementally and total size or per-item latency is unpredictable.
`React.cache` works inside server components. Use it when several async server components in one request need the same expensive fetch or computation.
That is request-scoped deduplication, not a cross-request cache.