Skip to content

Commit c7c4d60

Browse files
committed
header image
1 parent 131ae39 commit c7c4d60

File tree

4 files changed

+68
-9
lines changed

4 files changed

+68
-9
lines changed

media/brand.sketch

6.2 MB
Binary file not shown.
167 KB
Loading

src/blog/tanstack-start-rsc.md

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ authors:
66
- Manuel Schiller
77
---
88

9+
![What If RSCs Were Actually Components?](/blog-assets/tanstack-start-rsc/header.jpg)
10+
911
React Server Components are a genuine leap for React. They reduce bundle size, stream UI as it resolves, and move work off the client.
1012

11-
But the current model comes with a tradeoff: **the server owns the component tree**.
13+
But the way RSCs have been implemented so far comes with a tradeoff: **the server owns the component tree**.
1214
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.
1315

1416
What if it didn't have to?
@@ -19,9 +21,25 @@ That's what we built in **TanStack Start**.
1921

2022
---
2123

24+
## Why RSCs Matter
25+
26+
Before diving into the model, it's worth understanding when server components shine:
27+
28+
- **Heavy dependencies stay on the server.** Markdown parsers, syntax highlighters, date formatting libraries—these can add hundreds of KB to your bundle. With RSCs, that code runs on the server and only the rendered output ships to the client.
29+
30+
- **Colocated data fetching.** TanStack Router already eliminates waterfalls by parallelizing route loaders. RSCs offer a different ergonomic: awaiting data directly in the component that renders it, which can be convenient for static or slow-changing content.
31+
32+
- **Sensitive logic stays secure.** API keys, database queries, business logic—none of it reaches the client bundle.
33+
34+
- **Streaming for perceived performance.** Instead of waiting for all data before showing anything, RSCs stream UI progressively. Users see content immediately while slower parts load in the background.
35+
36+
RSCs aren't about replacing client interactivity—they're about choosing where work happens. **The question is: who controls that choice?**
37+
38+
---
39+
2240
## The One-Way Problem
2341

24-
In the traditional RSC model, the server is the only place that decides what interactive elements to render.
42+
In existing RSC implementations, the server is the only place that decides what interactive elements to render.
2543

2644
Need a `<Link>` with prefetching? **Server** renders it with `'use client'`.
2745
Need a dashboard widget? Server renders the boundary, marks it client. **The server is always the decision-maker. The client is always the recipient.**
@@ -39,7 +57,7 @@ In the traditional model, you'd create a new client component, a new file, a new
3957

4058
**Server components can render interactive elements directly**`<Link>`, `<Button>`, anything marked `'use client'`. That part works the same.
4159

42-
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.
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.
4361

4462
```tsx
4563
// Server
@@ -59,7 +77,9 @@ const getPost = createServerFn().handler(async ({ data }) => {
5977
<p>{post.body}</p>
6078

6179
{/* Server renders this directly—it's interactive via 'use client' */}
62-
<Link to={`/posts/${post.nextPostId}`}>Next Post</Link>
80+
<Link to="/posts/$postId" params={{ postId: post.nextPostId }}>
81+
Next Post
82+
</Link>
6383

6484
{/* Server defers this to the client */}
6585
<footer>
@@ -108,7 +128,11 @@ That's inversion of control.
108128

109129
Here's the shift: in TanStack Start, **server components aren't a paradigm**. They're a _primitive_—a serialization format that flows through your existing architecture.
110130

111-
When `createServerFn` returns a server component, that component is a **stream**. Streams are universal. They work with:
131+
When `createServerFn` returns a server component, that component is a **stream**. This means you don't have to wait for the entire component to finish rendering before sending HTML to the client. Parts of the UI can render immediately while slower async work (database queries, API calls) resolves in the background. The client sees a Suspense fallback, then the final content streams in when ready—no full page reload, no blocking.
132+
133+
This works for SSR (streaming HTML during initial page load) and for client-side fetches (streaming RSC payloads during navigation or data refetches).
134+
135+
Streams are universal. They work with:
112136

113137
- **TanStack Router** → Load server components in route loaders, stream them during navigation
114138
- **TanStack Query** → Cache server components, refetch in the background, deduplicate requests
@@ -155,6 +179,8 @@ Every property access and function call is tracked:
155179
- `props.children` → serialized as a slot placeholder
156180
- `props.renderActions({ postId, authorId })` → serialized with the arguments attached
157181

182+
You can destructure props normally—`({ children, renderActions })` works just as well as `props.children`. The proxy handles both patterns.
183+
158184
**Over the wire, it's a React element stream** with embedded placeholders. On the client:
159185

160186
1. The stream decodes into a React element tree
@@ -235,7 +261,7 @@ Because it's not about client or server.
235261

236262
This is the first experimental release of RSCs in TanStack Start. A few things to know:
237263

238-
**React's Flight serializer** — This release uses React's native RSC Flight protocol for serialization. That means TanStack Start's usual Seroval-based serialization isn't available within server components for now. Standard JavaScript primitives, Dates, and React elements serialize fine. Custom Seroval plugins and extended types will come in a future release as we unify the serialization layers.
264+
**React's Flight serializer** — This release uses React's native RSC Flight protocol for serialization. That means TanStack Start's usual serialization features aren't available within server components for now. Standard JavaScript primitives, Dates, and React elements serialize fine. Custom serialization plugins and extended types will come in a future release as we unify the serialization layers.
239265

240266
**API surface** — The `createServerComponent` API and slot patterns shown here are stable in design but may see refinements based on feedback. We're shipping early to learn from real usage.
241267

@@ -267,9 +293,42 @@ No. RSCs are entirely opt-in. You can build fully client-side SPAs with TanStack
267293

268294
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.
269295

270-
### When will Seroval serialization work inside RSCs?
296+
### When will TanStack Start's full serialization work inside RSCs?
297+
298+
It's on the roadmap. The current release uses React's Flight serializer directly, which handles the core use cases. Unifying with TanStack Start's serializer for custom types, extended serialization, and tighter TanStack DB integration is planned for a future release.
299+
300+
### Can I define my component outside of `createServerComponent`?
301+
302+
Yes. `createServerComponent` initiates the RSC stream generation, but your component can be defined separately and invoked inside:
303+
304+
```tsx
305+
function PostArticle({ post, children, renderActions }) {
306+
return (
307+
<article>
308+
<h1>{post.title}</h1>
309+
{renderActions?.({ postId: post.id })}
310+
{children}
311+
</article>
312+
)
313+
}
314+
315+
const getPost = createServerFn().handler(async ({ data }) => {
316+
const post = await db.posts.get(data.postId)
317+
return createServerComponent((props) => (
318+
<PostArticle post={post} {...props} />
319+
))
320+
})
321+
```
322+
323+
### Can I return raw JSX instead of using `createServerComponent`?
324+
325+
Not currently. Server functions that return UI must wrap it in `createServerComponent`. This is what enables the streaming, slot handling, and client rehydration. Plain JSX returned from a server function won't have the RSC serialization behavior.
326+
327+
### Do `cloneElement` and React Context work with server component children?
328+
329+
**`cloneElement`**: This won't work as you might expect. When children are passed from client to server, they're serialized as slot placeholders—not actual React elements the server can manipulate. The server can't inspect or clone client-provided children.
271330

272-
It's on the roadmap. The current release uses React's Flight serializer directly, which handles the core use cases. Unifying with Seroval for custom types, extended serialization, and tighter TanStack DB integration is planned for a future release.
331+
**React Context**: Context providers rendered by the server component _will_ wrap client children. If your server component renders `<ThemeProvider value="dark">{children}</ThemeProvider>`, the client children can consume that context. However, the context must be defined in a way that works across the server/client boundary (typically with `'use client'` on the provider component).
273332

274333
---
275334

src/components/DocsLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ function DocsMenuStrip({
128128
<div
129129
key={index}
130130
className={twMerge(
131-
'flex-1 min-h-[4px] max-h-[9px] rounded-sm',
131+
'flex-1 min-h-[4px] max-h-[9px] min-w-[20px] rounded-sm',
132132
item.isSection
133133
? 'w-full bg-current opacity-15'
134134
: isActive

0 commit comments

Comments
 (0)