server-components

Implement, review, debug, and refactor TanStack Start React Server Components in React 19 apps. Use when tasks mention @tanstack/react-start/rsc, renderServerComponent, createCompositeComponent, CompositeComponent, renderToReadableStream, createFromReadableStream, createFromFetch, Composite Components, React Flight streams, loader or query owned RSC caching, router.invalidate, structuralSharing: false, selective SSR, stale names like renderRsc or .validator, or migration from Next App Router RSC patterns. Do not use for generic SSR or non-TanStack RSC frameworks except brief comparison.

Install
npx skills add 'https://github.com/TanStack/router/tree/main/packages/react-start/skills/react-start/server-components'
Download bundle ↓
main · 8e164d2Scanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
← Back to SKILL.md

Caching, refresh, SSR modes, and loading patterns

Pick one cache owner first

Router cache

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.

TanStack Query

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:

structuralSharing: false

Without that, Query may try to merge RSC values across fetches.

HTTP / server cache

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(...).

Router-owned RSC pattern

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.

Query-owned RSC pattern

Use Query when you want explicit cache keys and background fetch behavior.

Recommended shape:

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:

await context.queryClient.ensureQueryData(postQueryOptions(params.postId))

Then read it with useSuspenseQuery in the component.

Refresh rules

  • 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.

Selective SSR modes

ssr: 'data-only'

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.

ssr: false

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.

Loading patterns

Independent fragments -> parallel

If the fragments do not share data, fetch them with separate server functions and Promise.all.

Shared data or shared invalidation -> bundle

If several fragments always share a fetch or invalidate together, return them from one server function. This reduces round trips and keeps ownership aligned.

One slow fragment should not block everything -> defer

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.

Large or unbounded lists -> async generators

Use async generators when items should arrive incrementally and total size or per-item latency is unpredictable.

Request-scoped memoization

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.

Referenced from SKILL.md