@tanstack/react-start 1.167.39 → 1.167.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-start",
3
- "version": "1.167.39",
3
+ "version": "1.167.41",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -131,12 +131,12 @@
131
131
  },
132
132
  "dependencies": {
133
133
  "pathe": "^2.0.3",
134
- "@tanstack/react-start-client": "1.166.38",
135
- "@tanstack/react-start-rsc": "0.0.18",
136
- "@tanstack/react-start-server": "1.166.39",
134
+ "@tanstack/react-start-client": "1.166.39",
135
+ "@tanstack/react-start-rsc": "0.0.20",
136
+ "@tanstack/react-start-server": "1.166.40",
137
137
  "@tanstack/router-utils": "^1.161.6",
138
- "@tanstack/start-plugin-core": "1.167.34",
139
- "@tanstack/react-router": "1.168.21",
138
+ "@tanstack/start-plugin-core": "1.167.35",
139
+ "@tanstack/react-router": "1.168.22",
140
140
  "@tanstack/start-client-core": "1.167.17",
141
141
  "@tanstack/start-server-core": "1.167.19"
142
142
  },
@@ -22,6 +22,8 @@ This skill builds on start-core. Read [start-core](../../../start-client-core/sk
22
22
 
23
23
  This skill covers the React-specific bindings, setup, and patterns for TanStack Start.
24
24
 
25
+ For React Server Components patterns, see [react-start/server-components](./server-components/SKILL.md).
26
+
25
27
  > **CRITICAL**: All code is ISOMORPHIC by default. Loaders run on BOTH server and client. Use `createServerFn` for server-only logic.
26
28
 
27
29
  > **CRITICAL**: Do not confuse `@tanstack/react-start` with Next.js or Remix. They are completely different frameworks with different APIs.
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: react-start/server-components
3
+ description: >-
4
+ Implement, review, debug, and refactor TanStack Start React Server
5
+ Components in React 19 apps. Use when tasks mention
6
+ @tanstack/react-start/rsc, renderServerComponent,
7
+ createCompositeComponent, CompositeComponent,
8
+ renderToReadableStream, createFromReadableStream, createFromFetch,
9
+ Composite Components, React Flight streams, loader or query owned
10
+ RSC caching, router.invalidate, structuralSharing: false,
11
+ selective SSR, stale names like renderRsc or .validator, or
12
+ migration from Next App Router RSC patterns. Do not use for
13
+ generic SSR or non-TanStack RSC frameworks except brief
14
+ comparison.
15
+ type: sub-skill
16
+ library: tanstack-start
17
+ library_version: '1.166.2'
18
+ requires:
19
+ - react-start
20
+ - start-core/server-functions
21
+ - start-core/execution-model
22
+ sources:
23
+ - TanStack/router:docs/start/framework/react/guide/server-components.md
24
+ - TanStack/router:docs/start/framework/react/guide/server-functions.md
25
+ - TanStack/router:docs/start/framework/react/guide/execution-model.md
26
+ - TanStack/router:docs/router/guide/data-loading.md
27
+ ---
28
+
29
+ # TanStack Start React Server Components
30
+
31
+ Treat TanStack Start RSCs as fetchable React Flight payloads, not as a framework-owned server tree. Start from data ownership and cache ownership, then choose the smallest RSC primitive that fits.
32
+
33
+ ## When this skill is active
34
+
35
+ 1. Inspect `vite.config.*` for `tanstackStart({ rsc: { enabled: true } })`, `rsc()`, and `viteReact()`.
36
+ 2. Inspect route files for `loader`, `loaderDeps`, `staleTime`, `ssr`, and `errorComponent`.
37
+ 3. Inspect server boundaries: `createServerFn`, `createServerOnlyFn`, `.server.*`, and imports from `@tanstack/react-start/server`.
38
+ 4. Identify the cache owner: Router loader cache, TanStack Query, or HTTP/server cache.
39
+ 5. Identify the refresh path: `router.invalidate()`, `invalidateQueries`, `refetchQueries`, or GET cache headers.
40
+
41
+ ## Hard invariants
42
+
43
+ - Route loaders are isomorphic. Do not put DB access, secrets, or Node-only APIs directly in a loader. If the loader itself must use browser APIs, make that route `ssr: false`.
44
+ - `renderServerComponent(...)` returns a renderable fragment. It does not support slots.
45
+ - `createCompositeComponent(...)` is for server-rendered UI that must accept client-provided `children`, render props, or component props.
46
+ - Query-cached RSC values require `structuralSharing: false`.
47
+ - Slot payloads are opaque on the server. Do not inspect, map, or clone `props.children`.
48
+ - Render-prop and component-slot arguments must stay Flight-serializable.
49
+ - Current server function validation API is `.inputValidator(...)`. Older snippets may still show `.validator(...)`; normalize them.
50
+ - TanStack custom serialization does not apply inside RSCs yet. Stay inside native Flight-supported values.
51
+
52
+ ## Decide three things immediately
53
+
54
+ ### 1) Transport / composition primitive
55
+
56
+ - No client slots needed -> `renderServerComponent`
57
+ - Client interactivity must be inserted inside server-rendered markup -> `createCompositeComponent` + `<CompositeComponent src={...} />`
58
+ - Need custom Flight streaming, API routes, or non-standard transport -> `renderToReadableStream`, `createFromReadableStream`, `createFromFetch`
59
+
60
+ ### 2) Cache owner
61
+
62
+ - Route-shaped data keyed by pathname, params, or search -> Router cache
63
+ - Independent key space, background refetch, or non-route ownership -> TanStack Query
64
+ - Cross-request reuse on server or CDN -> GET `createServerFn` + response cache headers and/or external server cache
65
+
66
+ ### 3) Refresh owner
67
+
68
+ - Router-owned RSC -> `router.invalidate()`
69
+ - Query-owned RSC -> `queryClient.invalidateQueries(...)` or `refetchQueries(...)`
70
+ - Mixed Router + Query -> invalidate both deliberately; do not assume one refreshes the other
71
+
72
+ ## Pattern chooser
73
+
74
+ - Simple server fragment in a route loader -> `renderServerComponent`
75
+ - Interactive slot inside server markup -> `createCompositeComponent`
76
+ - Route component needs browser APIs but loader can still prefetch on the server -> `ssr: 'data-only'`
77
+ - Loader itself needs browser APIs -> `ssr: false`
78
+ - Route cache key must include search params -> `loaderDeps`
79
+ - Query-managed RSC -> `useSuspenseQuery` + SSR `ensureQueryData`
80
+ - Multiple independent RSCs -> separate server functions + `Promise.all`
81
+ - Multiple RSCs sharing data or invalidating together -> one server function returning many renderables or sources
82
+ - Need isolated widget failures or staggered reveal -> return promises from the loader and resolve with `use()` inside Suspense
83
+
84
+ ## Slot choice
85
+
86
+ - `children`: free-form composition, no server-to-client data flow
87
+ - render props: the server must pass serializable data into client-rendered UI
88
+ - component props: reusable client slot with a stable typed prop surface
89
+ - If you are about to use `Children.map`, `cloneElement`, or inspect `children` on the server, stop and convert it to a render prop
90
+
91
+ ## Review / refactor checklist
92
+
93
+ - Is the chosen primitive the smallest one that fits?
94
+ - Does the loader own the RSC, or should Query own it?
95
+ - Are route cache keys complete (`params` + minimal `loaderDeps`)?
96
+ - Is invalidation hitting the real cache owner?
97
+ - Are query options using `structuralSharing: false` for any RSC value?
98
+ - Are mutations explicit `createServerFn({ method: 'POST' })` calls instead of hidden server actions?
99
+ - Are server-only imports kept inside server functions or server-only boundaries?
100
+ - Are examples using current names (`renderServerComponent`, `.inputValidator`) instead of stale ones?
101
+
102
+ ## Debug fast
103
+
104
+ - Setup, exports, or stale docs mismatch -> `docs/current-api-notes.md`
105
+ - Composite Component design or slot bug -> `docs/composite-components.md`
106
+ - Stale data, refetching, loader keys, Query vs Router ownership, or SSR mode -> `docs/caching-refresh-ssr.md`
107
+ - Review, refactor, import leaks, error boundaries, or serialization bugs -> `docs/debugging-review.md`
108
+ - Architecture and Next/App Router translation -> `docs/architecture.md`
109
+
110
+ ## Copy-paste patterns
111
+
112
+ - `examples/01-renderable-route-loader.tsx`
113
+ - `examples/02-composite-slots.tsx`
114
+ - `examples/03-query-owned-rsc.tsx`
115
+ - `examples/04-selective-ssr-data-only.tsx`
116
+ - `examples/05-ssr-false-browser-loader.tsx`
117
+ - `examples/06-low-level-flight-api-route.tsx`
118
+
119
+ ## Default implementation sequence
120
+
121
+ 1. Keep server-only work inside `createServerFn` or `createServerOnlyFn`
122
+ 2. Return an RSC from the server function
123
+ 3. Consume it through the route loader unless Query has a clear ownership advantage
124
+ 4. Add the smallest cache policy that satisfies freshness requirements
125
+ 5. Wire invalidation exactly once at the real cache owner
126
+ 6. Escalate to Composite Components only when the client must fill slots
127
+ 7. Escalate to low-level Flight APIs only when high-level helpers cannot express the transport
@@ -0,0 +1,84 @@
1
+ # Architecture and mental model
2
+
3
+ ## Core model
4
+
5
+ TanStack Start treats React Server Components as plain React Flight data:
6
+
7
+ - create them on the server
8
+ - return them from a server function or API route
9
+ - decode and render them during SSR or on the client
10
+ - cache them with whatever already owns the data flow
11
+
12
+ That matters because the framework is not asking you to move the whole UI tree to the server. RSCs are opt-in fragments that fit into existing Router and Query workflows.
13
+
14
+ ## Boundary model
15
+
16
+ The fastest way to get TanStack Start RSC wrong is to think “loader = server”.
17
+
18
+ Use these boundaries instead:
19
+
20
+ - `createServerFn`: explicit RPC boundary; safe to call from client code because the client gets an RPC stub
21
+ - `createServerOnlyFn`: utility that must never run on the client
22
+ - route `loader`: orchestration layer that runs on the server for the initial SSR request and in the browser on client navigation unless the route is `ssr: false`
23
+
24
+ Practical rule: loaders decide _when_ to fetch. Server functions decide _what_ must stay on the server.
25
+
26
+ ## What still works inside RSCs
27
+
28
+ Do not over-correct and assume RSCs are a stripped-down universe.
29
+
30
+ - TanStack Router `Link` works inside server components
31
+ - CSS Modules and global CSS imports work inside server components
32
+ - `React.cache` works for request-scoped memoization inside server components
33
+
34
+ Use those directly when they simplify the tree.
35
+
36
+ ## When RSCs actually pay off
37
+
38
+ Use TanStack Start RSCs when one or more of these are true:
39
+
40
+ - a region is heavy to render but mostly static once delivered
41
+ - a server-only dependency should never enter the client bundle
42
+ - data fetching and render logic want to live together
43
+ - you want progressively streamed HTML for part of the route
44
+ - the result can be cached better as a fragment than as an entire page
45
+
46
+ Do not force RSCs into highly interactive, state-dense client surfaces unless they materially reduce bundle size or remove awkward client/server sync.
47
+
48
+ ## Choosing tree ownership
49
+
50
+ Think in terms of ownership:
51
+
52
+ - client-owned UI: plain client components, Query, SPA patterns
53
+ - route-owned RSC: route loader fetches a fragment and Router cache owns freshness
54
+ - query-owned RSC: Query owns the fragment because its lifecycle is not route-shaped
55
+ - mixed ownership: the route owns coarse navigation state, Query owns sub-fragments
56
+
57
+ The wrong smell is accidental double ownership: one RSC is fetched in a loader, then also separately cached in Query, then only one side is invalidated.
58
+
59
+ ## Composite Components in one sentence
60
+
61
+ A Composite Component is a server-rendered fragment with placeholders the client fills later.
62
+
63
+ That makes it the replacement for “I need server-rendered markup here, but I still need client interactivity in the middle of it.”
64
+
65
+ The client can wrap, nest, reorder, and interleave those fragments instead of accepting a single framework-owned server tree.
66
+
67
+ ## Next App Router translation
68
+
69
+ Use this mapping when someone is thinking in Next terms:
70
+
71
+ - Next “server-first tree” -> Start “isomorphic-first app with opt-in RSC fragments”
72
+ - Next `'use client'` boundary -> Start Composite Component slot boundary
73
+ - Next server actions -> Start explicit `createServerFn({ method: 'POST' })`
74
+ - Next framework cache semantics -> Start Router cache, Query cache, and HTTP cache you control directly
75
+
76
+ The useful mental shift is this: in TanStack Start, RSC is a transport and composition primitive, not the center of gravity for the whole app.
77
+
78
+ ## High-value heuristics
79
+
80
+ - If the fragment never accepts client content, demote it to `renderServerComponent`.
81
+ - If a route-scoped RSC never needs independent refetching, keep it in the loader cache.
82
+ - If a fragment is reused across routes or refreshed independently, consider Query ownership.
83
+ - If several fragments always share data and invalidate together, bundle them in one server function.
84
+ - If one slow fragment blocks everything, stop awaiting it in the loader and defer it.
@@ -0,0 +1,137 @@
1
+ # Caching, refresh, SSR modes, and loading patterns
2
+
3
+ ## Pick one cache owner first
4
+
5
+ ### Router cache
6
+
7
+ Use Router when the RSC is route-shaped and keyed by URL state.
8
+
9
+ Relevant facts:
10
+
11
+ - Router loader cache keys are based on pathname and params, plus anything you add through `loaderDeps`
12
+ - default navigation `staleTime` is `0`
13
+ - default preload freshness is `30s`
14
+ - default stale reload mode is background stale-while-revalidate
15
+ - `router.invalidate()` reloads active loaders immediately and marks cached routes stale
16
+
17
+ Use Router ownership when the fragment naturally belongs to the route and should track navigation.
18
+
19
+ ### TanStack Query
20
+
21
+ Use Query when the fragment has its own lifecycle.
22
+
23
+ Typical reasons:
24
+
25
+ - reuse across routes
26
+ - background refetching independent of navigation
27
+ - manual query invalidation
28
+ - non-route ownership
29
+
30
+ Hard rule:
31
+
32
+ ```tsx
33
+ structuralSharing: false
34
+ ```
35
+
36
+ Without that, Query may try to merge RSC values across fetches.
37
+
38
+ ### HTTP / server cache
39
+
40
+ Use GET server functions and response headers when you want cross-request reuse:
41
+
42
+ - CDN caching
43
+ - edge caching
44
+ - reverse proxies
45
+ - shared server caches
46
+
47
+ Set headers in a GET `createServerFn` via `setResponseHeaders(...)`.
48
+
49
+ ## Router-owned RSC pattern
50
+
51
+ Use a server function to return the RSC, then fetch it in the route loader.
52
+
53
+ Tune Router with:
54
+
55
+ - `staleTime` when revisits should stay fresh for a window
56
+ - `loaderDeps` when search params affect the fragment
57
+ - `staleReloadMode: 'blocking'` when showing stale data during reload is unacceptable
58
+
59
+ `loaderDeps` rule: include only values the loader actually uses. Returning the whole search object causes noisy invalidation.
60
+
61
+ ## Query-owned RSC pattern
62
+
63
+ Use Query when you want explicit cache keys and background fetch behavior.
64
+
65
+ Recommended shape:
66
+
67
+ ```tsx
68
+ const postQueryOptions = (postId: string) => ({
69
+ queryKey: ['post-rsc', postId],
70
+ structuralSharing: false,
71
+ queryFn: () => getPostRsc({ data: { postId } }),
72
+ staleTime: 5 * 60 * 1000,
73
+ })
74
+ ```
75
+
76
+ For SSR reuse, prefetch in the route loader:
77
+
78
+ ```tsx
79
+ await context.queryClient.ensureQueryData(postQueryOptions(params.postId))
80
+ ```
81
+
82
+ Then read it with `useSuspenseQuery` in the component.
83
+
84
+ ## Refresh rules
85
+
86
+ - loader-owned fragment changed -> `router.invalidate()`
87
+ - query-owned fragment changed -> `queryClient.invalidateQueries(...)`
88
+ - shared route and query state -> refresh both if both are authoritative
89
+ - CDN or response-header cache involved -> make sure the mutation path also busts or bypasses server-side cache
90
+
91
+ Do not “spray” invalidation everywhere. Decide who owns freshness, then target that owner.
92
+
93
+ ## Selective SSR modes
94
+
95
+ ### `ssr: 'data-only'`
96
+
97
+ Use when:
98
+
99
+ - the route component needs browser APIs
100
+ - the loader can still fetch the RSC on the server for first paint
101
+ - you want server-fetched fragment data ready before the client component mounts
102
+
103
+ 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.
104
+
105
+ ### `ssr: false`
106
+
107
+ Use when the loader itself depends on browser APIs such as:
108
+
109
+ - `localStorage`
110
+ - `window`
111
+ - browser-only client state
112
+
113
+ In this mode, both the loader and component run in the browser. The loader can still call a server function that returns an RSC.
114
+
115
+ ## Loading patterns
116
+
117
+ ### Independent fragments -> parallel
118
+
119
+ If the fragments do not share data, fetch them with separate server functions and `Promise.all`.
120
+
121
+ ### Shared data or shared invalidation -> bundle
122
+
123
+ If several fragments always share a fetch or invalidate together, return them from one server function. This reduces round trips and keeps ownership aligned.
124
+
125
+ ### One slow fragment should not block everything -> defer
126
+
127
+ 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.
128
+
129
+ ### Large or unbounded lists -> async generators
130
+
131
+ Use async generators when items should arrive incrementally and total size or per-item latency is unpredictable.
132
+
133
+ ## Request-scoped memoization
134
+
135
+ `React.cache` works inside server components. Use it when several async server components in one request need the same expensive fetch or computation.
136
+
137
+ That is request-scoped deduplication, not a cross-request cache.
@@ -0,0 +1,115 @@
1
+ # Composite Components and slot composition
2
+
3
+ ## The real mechanism
4
+
5
+ Inside `createCompositeComponent`, slot props are placeholders, not real client elements.
6
+
7
+ Server-side behavior:
8
+
9
+ - reading `props.children` records “there will be children here”
10
+ - calling `props.renderSomething(args)` records “call this slot with these args later”
11
+ - reading a component prop like `props.AddToCart` records “render this client component here with these props later”
12
+
13
+ Client-side behavior:
14
+
15
+ - `<CompositeComponent src={...} ... />` replaces those placeholders with the real props you passed at render time
16
+
17
+ This is why slots are powerful and why some React habits stop working.
18
+
19
+ ## Choose the slot type by data flow
20
+
21
+ ### `children`
22
+
23
+ Use when:
24
+
25
+ - the server only needs a hole for client content
26
+ - no server data needs to flow into the slotted content
27
+ - free-form composition matters more than a rigid interface
28
+
29
+ Do not use when the server must inject IDs, permissions, pricing, or derived data into the child content.
30
+
31
+ ### render props
32
+
33
+ Use when:
34
+
35
+ - the server must pass data into the client-rendered content
36
+ - the data is serializable
37
+ - you want the call site to stay flexible
38
+
39
+ This is usually the best default when the server owns the data and the client owns the interactive control.
40
+
41
+ ### component props
42
+
43
+ Use when:
44
+
45
+ - you have a reusable client component
46
+ - the prop contract is stable
47
+ - you want the server to decide where it renders and which typed props it receives
48
+
49
+ This is a good fit for buttons, menus, controls, widgets, and repeated productized patterns.
50
+
51
+ ## Rules that matter
52
+
53
+ - `renderServerComponent` does not support slots
54
+ - do not use `React.Children.map`, `cloneElement`, or child inspection on the server
55
+ - render-prop arguments and component-slot props must be Flight-serializable
56
+ - keep slot contracts narrow; pass IDs and plain data, not giant objects by default
57
+
58
+ ## The most common anti-pattern
59
+
60
+ Bad instinct:
61
+
62
+ ```tsx
63
+ createCompositeComponent((props: { children?: React.ReactNode }) => (
64
+ <div>
65
+ {React.Children.map(props.children, (child) =>
66
+ React.cloneElement(child, { extra: 'prop' }),
67
+ )}
68
+ </div>
69
+ ))
70
+ ```
71
+
72
+ Correct rewrite:
73
+
74
+ ```tsx
75
+ createCompositeComponent<{
76
+ renderItem?: (data: { extra: string }) => React.ReactNode
77
+ }>((props) => <div>{props.renderItem?.({ extra: 'prop' })}</div>)
78
+ ```
79
+
80
+ Server-side slot content is opaque. If the server needs to add data, make the data explicit.
81
+
82
+ ## Good slot contracts
83
+
84
+ Prefer contracts like:
85
+
86
+ - `renderActions?: ({ postId, authorId }) => ReactNode`
87
+ - `AddToCart?: ComponentType<{ productId: string; price: number }>`
88
+ - `children?: ReactNode`
89
+
90
+ Avoid contracts like:
91
+
92
+ - `renderAnything?: (data: EntirePostRecordFromDB) => ReactNode`
93
+ - `children` plus child inspection and mutation
94
+ - opaque callbacks that expect non-serializable classes or functions from the server
95
+
96
+ ## Composition patterns that age well
97
+
98
+ ### Shell + actions
99
+
100
+ The server renders an article, card, or panel and exposes one `renderActions` slot for buttons, menus, or controls.
101
+
102
+ ### Shell + body children
103
+
104
+ The server renders stable framing UI and accepts `children` for optional interactive regions like comments, drawers, or editors.
105
+
106
+ ### Stable control slot
107
+
108
+ The server accepts a component prop such as `AddToCart`, `UserMenu`, or `RowActions` and supplies clean typed props to it.
109
+
110
+ ## Refactor heuristics
111
+
112
+ - No slot use anywhere -> replace the Composite Component with `renderServerComponent`
113
+ - Slot exists only to pass data -> prefer a render prop over `children`
114
+ - Repeated render-prop call sites with the same component -> promote to a component prop
115
+ - Giant slot arg object -> shrink it to the smallest serializable payload that the client really needs
@@ -0,0 +1,74 @@
1
+ # Current API notes and stale example corrections
2
+
3
+ Validated against official TanStack Start, TanStack Router, TanStack blog, and Vite docs on 2026-04-13.
4
+
5
+ ## Current setup shape
6
+
7
+ Use the current RSC setup:
8
+
9
+ - React 19+
10
+ - Vite 7+
11
+ - `@vitejs/plugin-rsc`
12
+ - `tanstackStart({ rsc: { enabled: true } })`
13
+ - `rsc()`
14
+ - `viteReact()`
15
+
16
+ ## Current high-level APIs
17
+
18
+ Prefer:
19
+
20
+ - `renderServerComponent`
21
+ - `createCompositeComponent`
22
+ - `CompositeComponent`
23
+
24
+ Use low-level APIs only for custom transport:
25
+
26
+ - `renderToReadableStream`
27
+ - `createFromReadableStream`
28
+ - `createFromFetch`
29
+
30
+ ## Current validation API
31
+
32
+ Use `.inputValidator(...)` on `createServerFn`.
33
+
34
+ Important because some current RSC docs snippets still show the older `.validator(...)` spelling. Normalize those examples before copying them into real code.
35
+
36
+ ## Current constraints
37
+
38
+ - RSC support is still experimental
39
+ - TanStack custom serialization is not available inside RSCs yet
40
+ - slot args must stay Flight-serializable
41
+ - `structuralSharing: false` is mandatory for Query-cached RSC values
42
+
43
+ ## Stale example traps to avoid
44
+
45
+ ### Old `renderRsc` examples
46
+
47
+ You may still find older official repo examples using `renderRsc` and older config shapes. Do not cargo-cult them into new code. Normalize to the current docs and current `@tanstack/react-start/rsc` APIs.
48
+
49
+ ### Old validation snippets
50
+
51
+ If you see `.validator(z.object(...))` in an RSC example, rewrite it to `.inputValidator(z.object(...))` to match the current server function API.
52
+
53
+ ### Old Vite config shapes
54
+
55
+ If an example enables Start but does not also install and register `@vitejs/plugin-rsc`, treat it as stale for current TanStack Start RSC setup.
56
+
57
+ ## Maintenance rule
58
+
59
+ When docs, examples, and repo snippets disagree:
60
+
61
+ 1. prefer the current TanStack Start docs
62
+ 2. prefer the current TanStack blog announcement over older repo samples
63
+ 3. cross-check with current Server Functions and Execution Model docs
64
+ 4. then normalize all local examples to one coherent modern API surface
65
+
66
+ ## Official source list
67
+
68
+ - TanStack Start Server Components docs
69
+ - TanStack Start Server Functions docs
70
+ - TanStack Start Execution Model docs
71
+ - TanStack Start Import Protection docs
72
+ - TanStack Router Data Loading docs
73
+ - TanStack blog: React Server Components Your Way
74
+ - Vite docs for `@vitejs/plugin-rsc`
@@ -0,0 +1,141 @@
1
+ # Debugging and review guide
2
+
3
+ ## Triage by failure stage
4
+
5
+ ### 1. Setup / build failure
6
+
7
+ Likely causes:
8
+
9
+ - `@vitejs/plugin-rsc` missing
10
+ - RSC not enabled in `tanstackStart({ rsc: { enabled: true } })`
11
+ - stale example using old exports
12
+ - wrong React or Vite baseline
13
+
14
+ Checks:
15
+
16
+ - inspect `vite.config.*`
17
+ - inspect imports from `@tanstack/react-start/rsc`
18
+ - normalize to `renderServerComponent`, `createCompositeComponent`, `CompositeComponent`, `.inputValidator(...)`
19
+
20
+ ### 2. Wrong environment failure
21
+
22
+ Symptoms:
23
+
24
+ - DB or secret access explodes on client navigation
25
+ - `window` or `localStorage` explodes during SSR
26
+ - import protection complaints about server-only code
27
+
28
+ Likely causes:
29
+
30
+ - server-only logic placed directly in an isomorphic loader
31
+ - browser-only logic used in SSR route/component
32
+ - helper referencing `.server` imports outside a recognized server boundary
33
+
34
+ Fixes:
35
+
36
+ - move server-only work into `createServerFn` or `createServerOnlyFn`
37
+ - use `ssr: 'data-only'` when only the component needs browser APIs
38
+ - use `ssr: false` when the loader itself needs browser APIs
39
+ - keep server-only imports inside server-only callbacks or files
40
+
41
+ ## Composition / slot bugs
42
+
43
+ Symptoms:
44
+
45
+ - child content disappears or cannot be modified
46
+ - `Children.map` or `cloneElement` does nothing useful
47
+ - component prop slot receives unusable data
48
+ - Composite Component seems heavier than necessary
49
+
50
+ Likely causes:
51
+
52
+ - trying to inspect server-side slot placeholders
53
+ - non-serializable slot args
54
+ - using a Composite Component for a fragment that has no slots
55
+
56
+ Fixes:
57
+
58
+ - replace child inspection with a render prop
59
+ - narrow slot args to serializable values
60
+ - demote slotless composites to `renderServerComponent`
61
+
62
+ ## Cache / staleness bugs
63
+
64
+ Symptoms:
65
+
66
+ - stale UI after mutation
67
+ - route revisits do not refresh when expected
68
+ - route refreshes too often
69
+ - Query errors or weird object merging on RSC values
70
+
71
+ Likely causes:
72
+
73
+ - invalidating the wrong cache owner
74
+ - missing or oversized `loaderDeps`
75
+ - missing `structuralSharing: false`
76
+ - `staleTime` / `staleReloadMode` mismatch with desired UX
77
+
78
+ Checks:
79
+
80
+ - decide whether Router or Query owns the fragment
81
+ - inspect `loaderDeps` for only the params actually used
82
+ - verify Query key includes all real inputs
83
+ - verify `structuralSharing: false`
84
+ - use `router.invalidate()` only when the loader owns freshness
85
+
86
+ ## Error boundary surprises
87
+
88
+ Rule:
89
+
90
+ - awaited loader failure -> route `errorComponent`
91
+ - deferred promise failure -> local ErrorBoundary around the deferred read
92
+
93
+ If a single failing widget should not take down the route, stop awaiting it in the loader.
94
+
95
+ ## Serialization bugs
96
+
97
+ Symptoms:
98
+
99
+ - odd runtime decode failures
100
+ - slot args rejected or malformed
101
+ - custom serializer assumptions fail
102
+
103
+ Likely causes:
104
+
105
+ - passing classes, functions, Maps/Sets with custom serialization expectations, or other non-Flight-friendly values
106
+ - relying on TanStack custom serialization inside RSCs
107
+
108
+ Fixes:
109
+
110
+ - reduce payloads to primitives, Dates, plain objects, arrays, and React elements where appropriate
111
+ - pass IDs, not rich server objects, unless they are plain and intentionally serialized
112
+
113
+ ## Import protection and bundling gotchas
114
+
115
+ - static imports of server functions are safe; the client build gets RPC stubs
116
+ - dynamic imports of server functions can cause bundler issues
117
+ - dev-mode import protection warnings can be informational because there is no tree-shaking; build output is the authoritative check
118
+
119
+ ## Code review checklist
120
+
121
+ - correct primitive: renderable vs composite vs low-level stream
122
+ - loader is orchestration, not secret storage
123
+ - `loaderDeps` only covers actual inputs
124
+ - Query-owned RSC uses `structuralSharing: false`
125
+ - invalidation targets the real cache owner
126
+ - mutations are explicit `POST` server functions
127
+ - slot contracts are narrow and serializable
128
+ - stale examples normalized to current APIs
129
+ - route-level vs component-level error handling matches the UX requirement
130
+
131
+ ## Simplify-first recovery path
132
+
133
+ When a bug is tangled across Query, Router, Composite Components, and SSR modes:
134
+
135
+ 1. collapse to one route-owned `renderServerComponent`
136
+ 2. verify the server function and loader path
137
+ 3. reintroduce Query only if you need independent ownership
138
+ 4. reintroduce Composite Components only if you need slots
139
+ 5. reintroduce deferred loading only if you need staggered reveal or isolated failures
140
+
141
+ This sequence removes entire classes of bugs instead of trying to patch all of them at once.
@@ -0,0 +1,39 @@
1
+ # Official sources
2
+
3
+ Validated on 2026-04-13.
4
+
5
+ ## TanStack Start
6
+
7
+ - Server Components guide
8
+ https://tanstack.com/start/latest/docs/framework/react/guide/server-components
9
+
10
+ - Server Functions guide
11
+ https://tanstack.com/start/latest/docs/framework/react/guide/server-functions
12
+
13
+ - Execution Model guide
14
+ https://tanstack.com/start/latest/docs/framework/react/guide/execution-model
15
+
16
+ - Import Protection guide
17
+ https://tanstack.com/start/latest/docs/framework/react/guide/import-protection
18
+
19
+ ## TanStack Router
20
+
21
+ - Data Loading guide
22
+ https://tanstack.com/router/latest/docs/framework/react/guide/data-loading
23
+
24
+ ## TanStack blog
25
+
26
+ - React Server Components Your Way
27
+ https://tanstack.com/blog/react-server-components
28
+
29
+ ## Vite
30
+
31
+ - `@vitejs/plugin-rsc` documentation / discovery entry point
32
+ https://vite.dev/plugins/
33
+
34
+ ## Notes on drift observed during validation
35
+
36
+ - Current RSC docs and blog use `renderServerComponent`, `createCompositeComponent`, `CompositeComponent`, and low-level Flight APIs from `@tanstack/react-start/rsc`.
37
+ - Current Server Functions docs use `.inputValidator(...)`.
38
+ - Some current RSC docs snippets still show older `.validator(...)`.
39
+ - Older official repo examples may still show `renderRsc` and older config shapes. Prefer the current docs above.
@@ -0,0 +1,28 @@
1
+ // Current TanStack Start RSC pattern.
2
+ // Replace route paths and data sources with your own app code.
3
+
4
+ import { createFileRoute } from '@tanstack/react-router'
5
+ import { createServerFn } from '@tanstack/react-start'
6
+ import { renderServerComponent } from '@tanstack/react-start/rsc'
7
+
8
+ function Greeting() {
9
+ return <h1>Hello from TanStack Start RSC</h1>
10
+ }
11
+
12
+ const getGreeting = createServerFn({ method: 'GET' }).handler(async () => {
13
+ const Renderable = await renderServerComponent(<Greeting />)
14
+ return { Renderable }
15
+ })
16
+
17
+ export const Route = createFileRoute('/hello')({
18
+ loader: async () => {
19
+ const { Renderable } = await getGreeting()
20
+ return { Greeting: Renderable }
21
+ },
22
+ component: HelloPage,
23
+ })
24
+
25
+ function HelloPage() {
26
+ const { Greeting } = Route.useLoaderData()
27
+ return <>{Greeting}</>
28
+ }
@@ -0,0 +1,124 @@
1
+ // Composite Component patterns:
2
+ // - children slot for free-form composition
3
+ // - render-prop slot when the server must pass data into client UI
4
+ // - component prop slot for reusable typed interactive controls
5
+
6
+ import type { ComponentType, ReactNode } from 'react'
7
+ import { createFileRoute } from '@tanstack/react-router'
8
+ import { createServerFn } from '@tanstack/react-start'
9
+ import {
10
+ CompositeComponent,
11
+ createCompositeComponent,
12
+ } from '@tanstack/react-start/rsc'
13
+ import { z } from 'zod'
14
+
15
+ // Replace with your own server-only data layer
16
+ declare const db: {
17
+ posts: {
18
+ findById(postId: string): Promise<{
19
+ id: string
20
+ authorId: string
21
+ title: string
22
+ body: string
23
+ }>
24
+ }
25
+ products: {
26
+ findById(productId: string): Promise<{
27
+ id: string
28
+ name: string
29
+ price: number
30
+ }>
31
+ }
32
+ }
33
+
34
+ const getPostCard = createServerFn({ method: 'GET' })
35
+ .inputValidator(z.object({ postId: z.string() }))
36
+ .handler(async ({ data }) => {
37
+ const post = await db.posts.findById(data.postId)
38
+
39
+ const src = await createCompositeComponent<{
40
+ children?: ReactNode
41
+ renderActions?: (args: { postId: string; authorId: string }) => ReactNode
42
+ }>((props) => (
43
+ <article className="card">
44
+ <h1>{post.title}</h1>
45
+ <p>{post.body}</p>
46
+
47
+ <footer>
48
+ {props.renderActions?.({
49
+ postId: post.id,
50
+ authorId: post.authorId,
51
+ })}
52
+ </footer>
53
+
54
+ <section>{props.children}</section>
55
+ </article>
56
+ ))
57
+
58
+ return { src }
59
+ })
60
+
61
+ export const Route = createFileRoute('/posts/$postId')({
62
+ loader: async ({ params }) =>
63
+ getPostCard({ data: { postId: params.postId } }),
64
+ component: PostPage,
65
+ })
66
+
67
+ function PostPage() {
68
+ const { src } = Route.useLoaderData()
69
+
70
+ return (
71
+ <CompositeComponent
72
+ src={src}
73
+ renderActions={({ postId, authorId }) => (
74
+ <PostActions postId={postId} authorId={authorId} />
75
+ )}
76
+ >
77
+ <Comments />
78
+ </CompositeComponent>
79
+ )
80
+ }
81
+
82
+ function Comments() {
83
+ return <div>Interactive comments go here</div>
84
+ }
85
+
86
+ function PostActions(props: { postId: string; authorId: string }) {
87
+ return (
88
+ <button type="button">
89
+ Moderate {props.postId} / {props.authorId}
90
+ </button>
91
+ )
92
+ }
93
+
94
+ // Component-prop slot variant
95
+ const getProductCard = createServerFn({ method: 'GET' })
96
+ .inputValidator(z.object({ productId: z.string() }))
97
+ .handler(async ({ data }) => {
98
+ const product = await db.products.findById(data.productId)
99
+
100
+ const src = await createCompositeComponent<{
101
+ AddToCart?: ComponentType<{ productId: string; price: number }>
102
+ }>((props) => (
103
+ <section className="product-card">
104
+ <h2>{product.name}</h2>
105
+ {props.AddToCart ? (
106
+ <props.AddToCart productId={product.id} price={product.price} />
107
+ ) : null}
108
+ </section>
109
+ ))
110
+
111
+ return { src }
112
+ })
113
+
114
+ function AddToCartButton(props: { productId: string; price: number }) {
115
+ return (
116
+ <button type="button">
117
+ Add {props.productId} at {props.price}
118
+ </button>
119
+ )
120
+ }
121
+
122
+ // Example usage somewhere else:
123
+ // const { src } = await getProductCard({ data: { productId: 'p-1' } })
124
+ // <CompositeComponent src={src} AddToCart={AddToCartButton} />
@@ -0,0 +1,97 @@
1
+ // Query-owned RSC pattern.
2
+ // Assumes your router context provides a queryClient for SSR prefetch.
3
+
4
+ import type { ReactNode } from 'react'
5
+ import { createFileRoute } from '@tanstack/react-router'
6
+ import { createServerFn } from '@tanstack/react-start'
7
+ import {
8
+ CompositeComponent,
9
+ createCompositeComponent,
10
+ } from '@tanstack/react-start/rsc'
11
+ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
12
+ import { z } from 'zod'
13
+
14
+ // Replace with your own data layer
15
+ declare const db: {
16
+ posts: {
17
+ findById(postId: string): Promise<{
18
+ id: string
19
+ title: string
20
+ body: string
21
+ }>
22
+ update(
23
+ postId: string,
24
+ patch: { title?: string; body?: string },
25
+ ): Promise<void>
26
+ }
27
+ }
28
+
29
+ const getPostRsc = createServerFn({ method: 'GET' })
30
+ .inputValidator(z.object({ postId: z.string() }))
31
+ .handler(async ({ data }) => {
32
+ const post = await db.posts.findById(data.postId)
33
+
34
+ const src = await createCompositeComponent<{
35
+ renderActions?: (args: { postId: string }) => ReactNode
36
+ }>((props) => (
37
+ <article>
38
+ <h1>{post.title}</h1>
39
+ <p>{post.body}</p>
40
+ <footer>{props.renderActions?.({ postId: post.id })}</footer>
41
+ </article>
42
+ ))
43
+
44
+ return { src }
45
+ })
46
+
47
+ const updatePost = createServerFn({ method: 'POST' })
48
+ .inputValidator(
49
+ z.object({
50
+ postId: z.string(),
51
+ title: z.string().optional(),
52
+ body: z.string().optional(),
53
+ }),
54
+ )
55
+ .handler(async ({ data }) => {
56
+ await db.posts.update(data.postId, {
57
+ title: data.title,
58
+ body: data.body,
59
+ })
60
+ })
61
+
62
+ const postQueryOptions = (postId: string) => ({
63
+ queryKey: ['post-rsc', postId],
64
+ structuralSharing: false,
65
+ queryFn: () => getPostRsc({ data: { postId } }),
66
+ staleTime: 5 * 60 * 1000,
67
+ })
68
+
69
+ export const Route = createFileRoute('/posts/$postId')({
70
+ loader: async ({ context, params }) => {
71
+ await context.queryClient.ensureQueryData(postQueryOptions(params.postId))
72
+ },
73
+ component: PostPage,
74
+ })
75
+
76
+ function PostPage() {
77
+ const { postId } = Route.useParams()
78
+ const queryClient = useQueryClient()
79
+
80
+ const { data } = useSuspenseQuery(postQueryOptions(postId))
81
+
82
+ const handleRename = async () => {
83
+ await updatePost({ data: { postId, title: 'Updated title' } })
84
+ await queryClient.invalidateQueries({ queryKey: ['post-rsc', postId] })
85
+ }
86
+
87
+ return (
88
+ <CompositeComponent
89
+ src={data.src}
90
+ renderActions={({ postId }) => (
91
+ <button type="button" onClick={handleRename}>
92
+ Refresh {postId}
93
+ </button>
94
+ )}
95
+ />
96
+ )
97
+ }
@@ -0,0 +1,72 @@
1
+ // Selective SSR: the loader fetches the RSC on the server,
2
+ // but the route component renders on the client because it needs browser APIs.
3
+
4
+ import * as React from 'react'
5
+ import type { ReactNode } from 'react'
6
+ import { createFileRoute } from '@tanstack/react-router'
7
+ import { createServerFn } from '@tanstack/react-start'
8
+ import {
9
+ CompositeComponent,
10
+ createCompositeComponent,
11
+ } from '@tanstack/react-start/rsc'
12
+
13
+ // Replace with your own server-side data source
14
+ declare function getDashboardStats(): Promise<{
15
+ series: Array<{ x: number; y: number }>
16
+ totalUsers: number
17
+ }>
18
+
19
+ const getDashboard = createServerFn({ method: 'GET' }).handler(async () => {
20
+ const stats = await getDashboardStats()
21
+
22
+ const src = await createCompositeComponent<{
23
+ renderChart?: (args: {
24
+ series: Array<{ x: number; y: number }>
25
+ }) => ReactNode
26
+ }>((props) => (
27
+ <section>
28
+ <h1>Users: {stats.totalUsers}</h1>
29
+ {props.renderChart?.({ series: stats.series })}
30
+ </section>
31
+ ))
32
+
33
+ return { src }
34
+ })
35
+
36
+ export const Route = createFileRoute('/dashboard')({
37
+ ssr: 'data-only',
38
+ loader: async () => ({
39
+ Dashboard: await getDashboard(),
40
+ }),
41
+ component: DashboardPage,
42
+ })
43
+
44
+ function DashboardPage() {
45
+ const { Dashboard } = Route.useLoaderData()
46
+ const [width, setWidth] = React.useState(0)
47
+
48
+ React.useEffect(() => {
49
+ setWidth(window.innerWidth)
50
+ }, [])
51
+
52
+ return (
53
+ <CompositeComponent
54
+ src={Dashboard.src}
55
+ renderChart={({ series }) => (
56
+ <ResponsiveChart data={series} width={width} />
57
+ )}
58
+ />
59
+ )
60
+ }
61
+
62
+ // Replace with your real chart component
63
+ function ResponsiveChart(props: {
64
+ data: Array<{ x: number; y: number }>
65
+ width: number
66
+ }) {
67
+ return (
68
+ <pre>
69
+ {JSON.stringify({ width: props.width, points: props.data.length })}
70
+ </pre>
71
+ )
72
+ }
@@ -0,0 +1,40 @@
1
+ // Browser-owned loader pattern.
2
+ // Use when the loader itself needs browser APIs such as localStorage.
3
+
4
+ import { createFileRoute } from '@tanstack/react-router'
5
+ import { createServerFn } from '@tanstack/react-start'
6
+ import { renderServerComponent } from '@tanstack/react-start/rsc'
7
+ import { z } from 'zod'
8
+
9
+ const getDrawingTools = createServerFn({ method: 'POST' })
10
+ .inputValidator(z.object({ savedState: z.string().nullable() }))
11
+ .handler(async ({ data }) => {
12
+ const Tools = await renderServerComponent(
13
+ <ToolPalette savedState={data.savedState} />,
14
+ )
15
+
16
+ return { Tools }
17
+ })
18
+
19
+ export const Route = createFileRoute('/canvas')({
20
+ ssr: false,
21
+ loader: async () => {
22
+ const savedState = localStorage.getItem('canvas-state')
23
+ return getDrawingTools({ data: { savedState } })
24
+ },
25
+ component: CanvasPage,
26
+ })
27
+
28
+ function CanvasPage() {
29
+ const { Tools } = Route.useLoaderData()
30
+ return <>{Tools}</>
31
+ }
32
+
33
+ function ToolPalette(props: { savedState: string | null }) {
34
+ return (
35
+ <section>
36
+ <h1>Canvas tools</h1>
37
+ <pre>{props.savedState ?? 'no saved state'}</pre>
38
+ </section>
39
+ )
40
+ }
@@ -0,0 +1,40 @@
1
+ // Low-level Flight stream APIs.
2
+ // Prefer high-level helpers unless you need a custom transport.
3
+
4
+ import { createAPIFileRoute } from '@tanstack/react-start/api'
5
+ import { createServerFn } from '@tanstack/react-start'
6
+ import {
7
+ createFromFetch,
8
+ renderToReadableStream,
9
+ } from '@tanstack/react-start/rsc'
10
+ import { useSuspenseQuery } from '@tanstack/react-query'
11
+
12
+ const getFlightStream = createServerFn({ method: 'GET' }).handler(async () => {
13
+ return renderToReadableStream(<div>Server rendered content</div>)
14
+ })
15
+
16
+ export const APIRoute = createAPIFileRoute('/api/rsc')({
17
+ GET: async () => {
18
+ const stream = await getFlightStream()
19
+
20
+ return new Response(stream, {
21
+ headers: {
22
+ 'Content-Type': 'text/x-component',
23
+ },
24
+ })
25
+ },
26
+ })
27
+
28
+ const rscQueryOptions = () => ({
29
+ queryKey: ['api-rsc'],
30
+ structuralSharing: false,
31
+ queryFn: async () => {
32
+ const Renderable = await createFromFetch(fetch('/api/rsc'))
33
+ return { Renderable }
34
+ },
35
+ })
36
+
37
+ export function ApiBackedRscWidget() {
38
+ const { data } = useSuspenseQuery(rscQueryOptions())
39
+ return <>{data.Renderable}</>
40
+ }