TanStack Start Now Support React Server Components

TanStack Start introduces a new way to use React Server Components by treating them as composable data streams rather than a rigid framework-owned architecture, giving developers full control over how server-rendered UI is managed.
by Manuel Schiller, Tanner Linsley, and Jack Herrington on Apr 13, 2026.
At TanStack, we have always strived to build tools that cover the 90% use case with ease, but still give you the flexibility to break out of the box for advanced use cases. Why? Because we know that when things get serious, you know what's best for your application and deserve the freedom to take control.
That's always been the TanStack philosophy, and we're happy that you've trusted us to take our time to deliver that same experience with React Server Components.
Yeah... no. This is not a server component crash course. If you are not familiar with Server Components by this point, please stop by the official React Server Components documentation for a few minutes to get acquainted.
RSCs are a necessary primitive for moving heavy or expensive rendering logic off the client and onto the server.
Especially static or infrequently changing content that can be cached consistently and granularly.
Markdown parsers, syntax highlighters, date formatting libraries, search indexing, content transforms, etc. are all great use cases, but certainly not the only ones.
They are a very powerful primitive.
Most people now think of RSCs in a server-first way: the server owns the tree, 'use client' marks the interactive parts, and the framework conventions decide how the whole thing fits together.
That model can be compelling. It makes streaming, server rendering, and colocated server-side work feel built in from the start.
But it also turns RSCs from a useful primitive into the thing your whole app has to orbit. The framework ends up owning how RSCs are created, where they render, how interactive boundaries are defined, and how UI gets recomposed when data or user actions change.
That is the part we kept getting hung up on. We do not think you should have to buy into that whole model up front just to get value out of RSCs.
What if you could use RSCs as granularly as you could fetch JSON on the client? In fact, what if the client decided how server-rendered UI gets fetched, cached, and composed in the first place?
In TanStack Start, the core idea is that RSCs are just streams of data that you can fetch, cache, and render on your terms at any time on the client instead of a server-owned component tree. That one shift makes them far more composable without changing anything fundamental about how they work.
With TanStack Start, RSCs are just React Flight streams. That sounds almost too obvious to say out loud, but that's exactly the point. We did not want them wrapped in a black-box convention with special rules, APIs, and network effects that change everything about the framework.
We wanted RSCs to behave more like any other piece of server data, which means nothing special should need to happen in a framework to support them outside of simply supporting streaming as a first-class citizen.
What does this mean in practice? You can:
Here is an RSC in TanStack Start:
Naturally, to cut down on server sync logic, we'll use TanStack Query to manage it!
import { createServerFn } from '@tanstack/react-start'
import {
createFromReadableStream,
renderToReadableStream,
} from '@tanstack/react-start/rsc'
// Create a server function
const getGreeting = createServerFn().handler(async () => {
// Create an RSC readable stream
return renderToReadableStream(
// Return JSX
<h1>Hello from the server</h1>,
)
})
function Greeting() {
const query = useQuery({
queryKey: ['greeting'],
queryFn: async () =>
// Create a renderable element from the stream
createFromReadableStream(
// Call our server function to get the stream
await getGreeting(),
),
})
// Render!
return <>{query.data}</>
}
At the primitive level, the API surface is intentionally small:
Under the hood, the story is straightforward: React renders to a Flight stream on the server, and the client decodes that stream back into a React element tree.
There is no secret extra protocol here. That is the primitive. Standard Flight streams in, standard React elements out.
That is enough to build a lot. You can treat RSC output like any other async resource in your app instead of a special-case framework-owned convention that you have to route every decision through.
Speaking of unnecessary framework convention: caching is not something new to reinvent, and RSCs are no exception.
When RSCs become "just data", the caching story gets a lot simpler. And because these are just granular streams delivered plainly over HTTP and handled transparently during rendering, they are not only easy to cache on the client, but also anywhere along the way on the server: in memory, in a database, behind a CDN, or wherever else your architecture already caches bytes, responses, or data.
This equally applies to caching layers you likely already know on the client as well, instead of requiring novel approaches and mental model shifts.
Let me explain.
TanStack Query illustrates this so well. It does not need a special "RSC mode". Once the RSC payload is part of an async query, you still get explicit cache keys, staleTime, background refetching, and the rest of Query's toolbox. For static content, just set staleTime: Infinity and you are done.
import { createServerFn } from '@tanstack/react-start'
import {
createFromReadableStream,
renderToReadableStream,
} from '@tanstack/react-start/rsc'
const getGreeting = createServerFn().handler(async () => {
return renderToReadableStream(<h1>Hello from the server</h1>)
})
function PostPage({ postId }: { postId: string }) {
const { data } = useSuspenseQuery({
queryKey: ['greeting-rsc', postId],
queryFn: async () => ({
Greeting: await createFromReadableStream(await getGreeting()),
}),
staleTime: 5 * 60 * 1000,
})
return <>{data.Greeting}</>
}
TanStack Router is even cooler. Because it natively supports streams, RSC payloads in route loaders are, again, "just data".
You can await them, stream them (yes, stream a stream), and naturally cache the result in the router cache just like any other loader output.
const getGreeting = createServerFn().handler(async () => {
return renderToReadableStream(<h1>Hello from the server</h1>)
})
export const Route = createFileRoute('/hello')({
loader: async () => ({
greeting: getGreeting(),
}),
component: function HelloPage() {
const { greeting } = Route.useLoaderData()
return <>{greeting}</>
},
})
Source: Hacker News















