Deep Dive: React Server Components in TanStack Start
Source: Dev.to
RSC fundamentals
Your component runs on the server. The server sends back the already‑rendered tree as data, called the RSC payload—not HTML, not JavaScript. The client reads the payload and renders it.
A raw RSC payload looks like this:
M1:{"id":"./ClientChart.js","chunks":["chunk1"],"name":""}
J0:["$","div",null,{"children":[
["$","h1",null,{"children":"Revenue"}],
["$","@1",null,{"data":{"today":18293.44}}]
]}]
M1indicates a client component.J0is the rendered tree.@1points back to that client component.
You can explore your own payload at https://rsc-parser.vercel.app.
If you need a refresher on RSC fundamentals, check out the recap video linked in the original post.
Under the hood
Two React APIs do the heavy lifting: renderToReadableStream and createFromReadableStream.
// create RSC payload
import { renderToReadableStream } from 'react-server-dom-webpack/server';
const stream = renderToReadableStream(, clientManifest);
// read RSC payload
import { createFromFetch } from 'react-server-dom-webpack/client';
const tree = await createFromFetch(fetch('/rsc?path=/dashboard'));
// then render {tree} inside your root
Everything a framework does with RSCs builds on these two functions.
How RSC differs from SSR
- SSR ships fully‑rendered HTML to the client, then sends JavaScript for hydration. The same component runs twice—once on the server, once on the client.
- RSC sends a lightweight payload instead of HTML. The component code never reaches the client; it runs only on the server.
React Server Components in Next.js
In Next.js, RSCs are enabled by default. You mark interactive parts of your app with the "use client" directive. When a request arrives, Next.js:
- Runs your Server Components on the server.
- Serializes the rendered tree into an RSC payload.
- Streams the payload to the client.
The framework abstracts away the creation and handling of RSCs, so you don’t see the underlying process.
How RSCs are different in TanStack Start
TanStack Start adopts a client‑first approach. The client decides whether to fetch a UI as fully rendered HTML or as streams of data. In TanStack Start, RSCs are simply React Flight streams—plain bytes—not a special framework construct. This lets you declaratively incorporate RSCs where you want them instead of being forced into an all‑RSC app.
Creating an RSC stream
import { renderToReadableStream } from '@tanstack/react-start/rsc';
const getGreeting = createServerFn().handler(async () => {
// Return an RSC readable stream containing JSX
return renderToReadableStream(
## Hello from the server
);
});
Consuming the RSC stream on the client
import { createFromReadableStream } from '@tanstack/react-start/rsc';
import { useQuery } from '@tanstack/react-query';
function Greeting() {
const query = useQuery({
queryKey: ['greeting'],
queryFn: async () =>
createFromReadableStream(await getGreeting()),
});
return <>{query.data};
}
(Example taken directly from the TanStack Start documentation.)
Why this matters
The TanStack approach keeps RSCs transparent and declarative. You know exactly which streams are RSCs, allowing you to sprinkle them into your app as needed rather than having the entire app default to RSCs.