Skip to content

Commit 97fb0ec

Browse files
checkpoint (#616)
Co-authored-by: Manuel Schiller <[email protected]>
1 parent 3c2535c commit 97fb0ec

File tree

1 file changed

+36
-14
lines changed

1 file changed

+36
-14
lines changed

src/blog/tanstack-start-rsc.md

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ authors:
1111
React Server Components are a genuine leap for React. They reduce bundle size, stream UI as it resolves, and move work off the client.
1212

1313
But the way RSCs have been implemented so far comes with a tradeoff: **the server owns the component tree**.
14-
Your client code opts into interactivity with `'use client'`. **Composition flows one direction**—server decides, client receives. The React model you know—props, context, bidirectional composition—gets fragmented across environments.
14+
Your client code opts into interactivity with `'use client'`. **Composition flows one direction**—server decides, client receives. The React model you know—props, context, client-led composition—gets fragmented across environments.
1515

1616
What if it didn't have to?
1717

18-
What if RSCs were actually components? **Fetchable. Cacheable. Composable in both directions.** Primitives that flow through your router, your cache, your data layer—without special directives or framework lock-in.
18+
What if RSCs were actually components? **Fetchable. Cacheable. Composable by the client.** Primitives that flow through your router, your cache, your data layer—without special directives or framework lock-in.
1919

2020
That's what we built in **TanStack Start**.
2121

@@ -51,13 +51,24 @@ In the traditional model, you'd create a new client component, a new file, a new
5151

5252
---
5353

54-
## Bidirectional Composition
54+
## Composite Components
5555

56-
**TanStack Start flips the constraint.**
56+
**TanStack Start treats RSC output as a composable UI value.**
5757

58-
**Server components can render interactive elements directly**`<Link>`, `<Button>`, anything marked `'use client'`. That part works the same.
58+
A **Composite Component** is a server-produced React component that the client can fetch, cache, stream, and then **assemble into its own UI tree**. It’s not “the server owns the tree and the client hydrates what it gets”. It’s the server shipping UI _pieces_ that remain composable at the client.
5959

60-
But they can also **declare slots**—regions where the client decides what to render. Not by creating new components or files, but through plain props: children, render functions, whatever pattern you already use. These aren't special APIs—`renderActions` in the example below is just a prop name we chose. You can name your render props anything you want.
60+
Composite Components give you two levels of composition:
61+
62+
### 1) Intra-component slotting (composition inside one component)
63+
64+
A Composite Component can render real server UI, _and_ expose **slots** using plain React patterns:
65+
66+
- `children`
67+
- render props (like `renderActions`)
68+
69+
These aren’t framework-specific APIs. They’re just props.
70+
71+
The important nuance: client-provided UI crosses the boundary as **opaque slot placeholders**. The server can position those placeholders (because it owns the frame), but it can’t inspect, clone, or transform client subtrees. That’s what keeps the model predictable.
6172

6273
```tsx
6374
// Server
@@ -81,12 +92,12 @@ const getPost = createServerFn().handler(async ({ data }) => {
8192
Next Post
8293
</Link>
8394

84-
{/* Server defers this to the client */}
95+
{/* Server requests client UI at a specific point */}
8596
<footer>
8697
{props.renderActions?.({ postId: post.id, authorId: post.authorId })}
8798
</footer>
8899

89-
{/* Client decides what goes here */}
100+
{/* Client fills this slot */}
90101
{props.children}
91102
</article>
92103
),
@@ -117,10 +128,21 @@ function PostPage({ postId }) {
117128
}
118129
```
119130

120-
**The server rendered the `<Link>` directly.** It also passed `postId` and `authorId` through the slot so the client could render `<PostActions>` with that data. And it left children open for the client to fill with `<Comments>`.
131+
Here the server renders the `<Link>` directly, but leaves **join points** for the client:
132+
133+
- a render prop slot for `<PostActions>` (with server-provided arguments)
134+
- a `children` slot for `<Comments>`
135+
136+
### 2) Inter-component composition (composition across many components)
137+
138+
Once a Composite Component is just “data you can render”, the client can treat it like a building block:
139+
140+
- interleave multiple Composite Components in a brand-new tree
141+
- wrap them in client providers/layout
142+
- nest one Composite Component inside another by passing it through slots
143+
- reorder/swap them based on client state
121144

122-
**Both directions. Same component. No new files. No new boundaries.**
123-
That's inversion of control.
145+
Same component. Same mental model. No new boundaries for every deferral.
124146

125147
---
126148

@@ -249,7 +271,7 @@ But the traditional model makes a choice: **the server owns the tree, and intera
249271

250272
> RSCs are a serialization format—a way to stream React elements from the server. They're a primitive, not a paradigm. Use them when they help. Compose around them when you need control. The client and server are peers, not a hierarchy.
251273
252-
The server can render interactive elements directly. It can also defer to the client through slots. **Composition flows both directions.**
274+
The server can render interactive elements directly. It can also defer to the client through slots. **The client still owns the final composition.**
253275
You decide the balance, per-component, based on what makes sense.
254276

255277
Because it's not about client or server.
@@ -277,7 +299,7 @@ If you hit rough edges, [open an issue](https://github.com/tanstack/router/issue
277299

278300
Next.js App Router is server-first: your component tree lives on the server by default, and you opt into client interactivity with `'use client'` boundaries.
279301

280-
TanStack Start is **isomorphic-first**: your tree lives wherever makes sense, and RSCs are a primitive you pull in when helpful. The key difference is **bidirectional composition**server components can defer to the client through slots, not just the other way around.
302+
TanStack Start is **isomorphic-first**: your tree lives wherever makes sense, and RSCs are a primitive you pull in when helpful. The key difference is **client-led composition**Composite Components expose slots so the client can assemble the final tree, not just accept a server-owned one.
281303

282304
Both approaches support RSCs. They differ in who owns the tree by default and how composition flows.
283305

@@ -289,7 +311,7 @@ Not directly—TanStack Start is its own framework built on TanStack Router. But
289311

290312
No. RSCs are entirely opt-in. You can build fully client-side SPAs with TanStack Start, use traditional SSR without server components, or go fully static. RSCs are one tool in the spectrum, not a requirement.
291313

292-
### What about React 19 / `use` / Server Actions?
314+
### What about React 19 / `use server` / Server Actions?
293315

294316
TanStack Start's RSC implementation builds on React's Flight protocol and works with React 19. Server Actions are a separate primitive—`createServerFn` serves a similar purpose but integrates with TanStack's middleware, validation, and caching model. We're watching the Server Actions API and will align where it makes sense.
295317

0 commit comments

Comments
 (0)