@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 +6 -6
- package/skills/react-start/SKILL.md +2 -0
- package/skills/react-start/server-components/SKILL.md +127 -0
- package/skills/react-start/server-components/docs/architecture.md +84 -0
- package/skills/react-start/server-components/docs/caching-refresh-ssr.md +137 -0
- package/skills/react-start/server-components/docs/composite-components.md +115 -0
- package/skills/react-start/server-components/docs/current-api-notes.md +74 -0
- package/skills/react-start/server-components/docs/debugging-review.md +141 -0
- package/skills/react-start/server-components/docs/sources.md +39 -0
- package/skills/react-start/server-components/examples/01-renderable-route-loader.tsx +28 -0
- package/skills/react-start/server-components/examples/02-composite-slots.tsx +124 -0
- package/skills/react-start/server-components/examples/03-query-owned-rsc.tsx +97 -0
- package/skills/react-start/server-components/examples/04-selective-ssr-data-only.tsx +72 -0
- package/skills/react-start/server-components/examples/05-ssr-false-browser-loader.tsx +40 -0
- package/skills/react-start/server-components/examples/06-low-level-flight-api-route.tsx +40 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-start",
|
|
3
|
-
"version": "1.167.
|
|
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.
|
|
135
|
-
"@tanstack/react-start-rsc": "0.0.
|
|
136
|
-
"@tanstack/react-start-server": "1.166.
|
|
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.
|
|
139
|
-
"@tanstack/react-router": "1.168.
|
|
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
|
+
}
|