{"id":9689,"date":"2026-05-18T09:16:41","date_gmt":"2026-05-18T14:16:41","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=9689"},"modified":"2026-05-18T09:16:42","modified_gmt":"2026-05-18T14:16:42","slug":"react-server-components-in-tanstack","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/react-server-components-in-tanstack\/","title":{"rendered":"React Server Components in TanStack"},"content":{"rendered":"\n<p>This post is about <a href=\"https:\/\/react.dev\/reference\/rsc\/server-components\">React Server Components<\/a> (or RSC) in <a href=\"https:\/\/tanstack.com\/start\/latest\">TanStack Start<\/a>. The implementation is radically different, and in my opinion, better than the RSC implementation you&#8217;ve likely seen in Next.js.<\/p>\n\n\n\n<p>This post will not be a direct 1:1 comparison. Instead, I&#8217;ll introduce this feature from first principles, as it exists in TanStack.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What are React Server Components<\/h2>\n\n\n\n<p>Server Components are normal React components with one key feature: they run on the server, <em>and only<\/em> on the server. This leads to a few key differences.<\/p>\n\n\n\n<p>RSCs can be async and can request data directly from within the component. It can <code>await<\/code>, well, anything that yields data. That could be a fetch from a 3rd-party API, or a direct call to your database. Since RSCs only run on the server, you don&#8217;t have to worry about your browser hopelessly failing to establish a TCP connection to your Postgres box, nor do you have to worry about secrets like connection strings being exposed to end users.<\/p>\n\n\n\n<p>The other key difference with RSC is hidden in plain sight, as we&#8217;ve already discussed: since these components only ever run on the server, their code will never be shipped to the client. RSCs simply send the final rendered markup, without the code that created it, to your client bundles.<\/p>\n\n\n\n<p>Since RSCs only exist on the server, they cannot have any state or user-facing interactivity. They cannot use hooks like <code>useState<\/code>, or have event handlers like onClick. If you need to integrate interactive content like that with RSC, you can, and we&#8217;ll go over how. But the RSCs themselves are React components that exist to run on the Server, and generate static content that&#8217;s shipped to the client (possibly with client components intermixed).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What RSC is Not<\/h2>\n\n\n\n<p>Don&#8217;t be mistaken<span style=\"box-sizing: border-box; margin: 0px; padding: 0px;\">: RSC is&nbsp;<em>not<\/em>&nbsp;a solution for loading<\/span> data more conveniently. TanStack Start already ships extremely simple, streamlined data-loading options. You have nested, isomorphic loaders for every level in your routing hierarchy. These loaders run on the server for your initial render and then on the client thereafter. This enables the deep integration with <a href=\"https:\/\/tanstack.com\/query\/latest\">react-query<\/a> TanStack Start offers, along with fine-grained data invalidation. I wrote all about this in a <a href=\"https:\/\/frontendmasters.com\/blog\/introducing-tanstack-start\/\">previous introduction to TanStack Start<\/a>.<\/p>\n\n\n\n<p>RSC is also not a way to server-render content. TanStack Start (and Next.js for that matter), <em>already<\/em> server renders your initial navigation, and always has. Your normal, old-school components always render on the server, and then re-render on the client, wiring up event handlers and effects in a process known as &#8220;hydration.&#8221; RSCs also render on the server, but they <em>only<\/em> render on the server.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Where RSC Shines<\/h2>\n\n\n\n<p>By rendering only on the server, your client bundles avoid the cost of all the code required to render your content. That means component trees that are large and expensive, with minimal client-side interactivity, are a prime candidate.<\/p>\n\n\n\n<p>The original <a href=\"https:\/\/tanstack.com\/blog\/react-server-components\">blog post announcement<\/a> for TanStack&#8217;s RSCs discussed using them for content with code samples. By moving the code to parse, style, and format displayed code to the server, those libraries were removed from client-side bundles, saving non-trivial amounts of space.<\/p>\n\n\n\n<p>In this post, we&#8217;ll simulate another good use case: content that&#8217;s mostly non-interactive, with many conditional imports and conditional rendering. Imagine an application shell, or layout, that can look lots of different ways depending on who&#8217;s viewing it: non-authenticated users, authenticated users, admin users, or even just authenticated users with varying permissions, which affect the content they&#8217;re shown.<\/p>\n\n\n\n<p>To keep things simple, we&#8217;ll build a dirt-simple application layout, but use some trickery to bloat the component bundle, so we can see how much lighter it is when we switch to RSC. We&#8217;ll then see about adding interactivity.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Getting Started<\/h2>\n\n\n\n<p>Check <a href=\"https:\/\/tanstack.com\/start\/v0\/docs\/framework\/react\/guide\/server-components#setup\">the docs<\/a> for instructions&nbsp;on configuring Vite for RSC.<\/p>\n\n\n\n<p>The repo for what we&#8217;ll be building <a href=\"https:\/\/github.com\/arackaf\/tanstack-start-rsc-blog-post\">is here<\/a>. It&#8217;s essentially an empty web application, with a skeleton layout that looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"772\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img1.jpg?resize=1024%2C772&#038;ssl=1\" alt=\"\" class=\"wp-image-9698\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img1.jpg?resize=1024%2C772&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img1.jpg?resize=300%2C226&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img1.jpg?resize=768%2C579&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img1.jpg?resize=1536%2C1157&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img1.jpg?resize=2048%2C1543&amp;ssl=1 2048w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>If the icons in the side panel don&#8217;t make much sense, it&#8217;s because they&#8217;re randomly chosen in a way that guarantees the entirety of the <a href=\"https:\/\/lucide.dev\/guide\/react\/\">lucide-react<\/a> icon package cannot be tree shaken. This is how we&#8217;re simulating a large component tree that&#8217;s not needed on the client.<\/p>\n\n\n\n<p>In the header, the avatar is clickable and opens a side panel, driven by the <a href=\"https:\/\/ui.shadcn.com\/docs\/components\/radix\/sidebar\">shadcn\/ui Sidebar<\/a> component.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"772\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img2.jpg?resize=1024%2C772&#038;ssl=1\" alt=\"\" class=\"wp-image-9699\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img2.jpg?resize=1024%2C772&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img2.jpg?resize=300%2C226&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img2.jpg?resize=768%2C579&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img2.jpg?resize=1536%2C1157&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img2.jpg?resize=2048%2C1543&amp;ssl=1 2048w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">The Normal Way<\/h2>\n\n\n\n<p>Building out this UI with standard, non-RSC components is a familiar process for anyone who&#8217;s worked with TanStack.<\/p>\n\n\n\n<p>We render our application shell from our root component, which handles the root layout. To simulate loading our logged-in user, we&#8217;ll add a loader to this same root layout.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">loader: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> user = <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Promise<\/span>&lt;{ name: <span class=\"hljs-built_in\">string<\/span>; avatar: <span class=\"hljs-built_in\">string<\/span> }&gt;<span class=\"hljs-function\">(<span class=\"hljs-params\">(<span class=\"hljs-params\">res<\/span><\/span>) =&gt;<\/span> {\n    setTimeout(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n      res({ name: <span class=\"hljs-string\">\"Adam Rackis\"<\/span>, avatar: <span class=\"hljs-string\">\"https:\/\/d193qjyckdxivp.cloudfront.net\/avatar.jpg\"<\/span> });\n    }, <span class=\"hljs-number\">1000<\/span>);\n  });\n  <span class=\"hljs-keyword\">return<\/span> { user };\n},<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We won&#8217;t mess with real data, just a (long) manual delay, and we send data back. Actually, we send our&nbsp;data&nbsp;<em>in<\/em>&nbsp;a&nbsp;<em>promise<\/em>. TanStack Start allows us to return promises from loaders, which get streamed to the UI once ready. This will be a nice opportunity for us to see Suspense-based streaming both with and without RSC.<\/p>\n\n\n\n<p>And here&#8217;s our non-RSC application shell component<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">type<\/span> ApplicationShellProps = {\n  user: <span class=\"hljs-built_in\">Promise<\/span>&lt;{\n    name: <span class=\"hljs-built_in\">string<\/span>;\n    avatar: <span class=\"hljs-built_in\">string<\/span>;\n  }&gt;;\n};\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> ApplicationShellNonRSC: FC&lt;PropsWithChildren&lt;ApplicationShellProps&gt;&gt; = <span class=\"hljs-function\"><span class=\"hljs-params\">props<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> { children, user } = props;\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;main className=<span class=\"hljs-string\">\"h-screen\"<\/span>&gt;\n      &lt;header className=<span class=\"hljs-string\">\"fixed top-0 left-0 right-0 h-12 z-10 bg-blue-200 flex items-center px-4 gap-4\"<\/span>&gt;\n        &lt;Suspense fallback={&lt;span className=<span class=\"hljs-string\">\"w-6 h-6 bg-gray-400 rounded-full\"<\/span>&gt;&lt;<span class=\"hljs-regexp\">\/span&gt;}&gt;\n          &lt;UserHeaderMenu user={user} \/<\/span>&gt;\n        &lt;<span class=\"hljs-regexp\">\/Suspense&gt;\n        &lt;span&gt;Header&lt;\/<\/span>span&gt;\n      &lt;<span class=\"hljs-regexp\">\/header&gt;\n      &lt;section className=\"fixed left-0 top-12 bottom-0 w-60 overflow-auto \"&gt;\n        &lt;SideBarContent \/<\/span>&gt;\n      &lt;<span class=\"hljs-regexp\">\/section&gt;\n      &lt;section className=\"max-w-&#91;600px] pt-16 mx-auto h-full\"&gt;\n        &lt;div className=\"flex flex-col gap-2 h-full\"&gt;\n          &lt;section className=\"min-h-&#91;200px]\"&gt;{children}&lt;\/<\/span>section&gt;\n          &lt;footer className=<span class=\"hljs-string\">\"px-4 fixed bottom-0 left-0 right-0 h-12 z-10 bg-blue-200 flex gap-4 items-center\"<\/span>&gt;&lt;<span class=\"hljs-regexp\">\/footer&gt;\n        &lt;\/<\/span>div&gt;\n      &lt;<span class=\"hljs-regexp\">\/section&gt;\n    &lt;\/m<\/span>ain&gt;\n  );\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Notice that we pass that same <em>promise<\/em> with our user data over to <code>UserHeaderMenu<\/code>, which itself is wrapped in a Suspense tag. Here&#8217;s that component.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> UserHeaderMenu: FC&lt;{ user: <span class=\"hljs-built_in\">Promise<\/span>&lt;{ name: <span class=\"hljs-built_in\">string<\/span>; avatar: <span class=\"hljs-built_in\">string<\/span> }&gt; }&gt; = <span class=\"hljs-function\"><span class=\"hljs-params\">props<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> { user } = props;\n  <span class=\"hljs-keyword\">const<\/span> { name, avatar } = use(user);\n\n  <span class=\"hljs-keyword\">return<\/span> &lt;SidePanelTrigger name={name} avatar={avatar} \/&gt;;\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We call <code>use<\/code> on the user info promise, a special pseudo-hook exported by React (version 19 and beyond). The <code>use<\/code> function causes our component to suspend and render the fallback from the <code>Suspense<\/code> tag.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"361\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img3.jpg?resize=1024%2C361&#038;ssl=1\" alt=\"\" class=\"wp-image-9700\" style=\"width:600px\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img3.jpg?resize=1024%2C361&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img3.jpg?resize=300%2C106&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img3.jpg?resize=768%2C271&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img3.jpg?w=1072&amp;ssl=1 1072w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>When the data are ready, the promise resolves, and our content shows our full UI.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"352\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img4.jpg?resize=1024%2C352&#038;ssl=1\" alt=\"\" class=\"wp-image-9701\" style=\"width:600px\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img4.jpg?resize=1024%2C352&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img4.jpg?resize=300%2C103&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img4.jpg?resize=768%2C264&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/img4.jpg?w=1088&amp;ssl=1 1088w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">The Non-RSC Payload<\/h2>\n\n\n\n<p>As I said above, I&#8217;ve used some trickery to force the entire Lucide React package to be bundled, simulating a deeply nested component hierarchy.<\/p>\n\n\n\n<p>On a production build, a total of 308 KB of JavaScript is sent down.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rendering with RSC<\/h2>\n\n\n\n<p>Let&#8217;s start with the simplest possible RSC component, which takes no props. It won&#8217;t even take children, which itself is just a prop. Here&#8217;s a new version of our application shell.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { <span class=\"hljs-keyword\">type<\/span> FC } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { SideBarContent } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/SideBarContent\"<\/span>;\n\n<span class=\"hljs-keyword\">type<\/span> ApplicationShellProps = {};\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> ApplicationShellEmptyRSC: FC&lt;ApplicationShellProps&gt; = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;main className=<span class=\"hljs-string\">\"h-screen\"<\/span>&gt;\n      &lt;header className=<span class=\"hljs-string\">\"fixed top-0 left-0 right-0 h-12 z-10 bg-blue-200 flex items-center px-4 gap-4\"<\/span>&gt;\n        &lt;span&gt;Header&lt;<span class=\"hljs-regexp\">\/span&gt;\n      &lt;\/<\/span>header&gt;\n      &lt;section className=<span class=\"hljs-string\">\"fixed left-0 top-12 bottom-0 w-60 overflow-auto \"<\/span>&gt;\n        &lt;SideBarContent \/&gt;\n      &lt;<span class=\"hljs-regexp\">\/section&gt;\n      &lt;section className=\"max-w-&#91;600px] pt-16 mx-auto h-full\"&gt;\n        &lt;div className=\"flex flex-col gap-2 h-full\"&gt;\n          &lt;section className=\"min-h-&#91;200px]\"&gt;&lt;\/<\/span>section&gt;\n          &lt;footer className=<span class=\"hljs-string\">\"px-4 fixed bottom-0 left-0 right-0 h-12 z-10 bg-blue-200 flex gap-4 items-center\"<\/span>&gt;&lt;<span class=\"hljs-regexp\">\/footer&gt;\n        &lt;\/<\/span>div&gt;\n      &lt;<span class=\"hljs-regexp\">\/section&gt;\n    &lt;\/m<\/span>ain&gt;\n  );\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>To be clear, this component is useless. It does not display our header, nor does it display the actual, currently rendered page (via children). But it will let us see how to render an RSC that takes no props.<\/p>\n\n\n\n<p>Let&#8217;s see how to render it as an RSC.<\/p>\n\n\n\n<p>First, we&#8217;ll import it into our root layout (or any layout, or route, or component), as well as a new helper from TanStack<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { ApplicationShellEmptyRSC } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"#\/components\/ApplicationShellEmptyRSC\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { renderServerComponent } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-start\/rsc\"<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then we&#8217;ll create a <code>serverFn<\/code> to turn this component into an RSC stream.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> getAppShell = createServerFn({\n  method: <span class=\"hljs-string\">\"GET\"<\/span>,\n}).handler(<span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">return<\/span> renderServerComponent(&lt;ApplicationShellEmptyRSC \/&gt;);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now we just need to <em>call<\/em> our server function. We can do this anywhere. For our purposes, we&#8217;ll just call it in our loader and send the result down.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">  loader: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> appShell = <span class=\"hljs-keyword\">await<\/span> getAppShell();\n    <span class=\"hljs-keyword\">return<\/span> { appShell };\n  },<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>In our React component, we grab that payload and render it.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">RootDocument<\/span>(<span class=\"hljs-params\">{ children }: { children: React.ReactNode }<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> { appShell } = Route.useLoaderData();\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;html lang=<span class=\"hljs-string\">\"en\"<\/span> suppressHydrationWarning&gt;\n      &lt;head&gt;\n        &lt;HeadContent \/&gt;\n      &lt;<span class=\"hljs-regexp\">\/head&gt;\n      &lt;body&gt;\n        {\/<\/span>* render the RSC *<span class=\"hljs-regexp\">\/}\n        {appShell}\n      &lt;\/<\/span>body&gt;\n    &lt;<span class=\"hljs-regexp\">\/html&gt;\n  );\n}<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This works! Simple as that.<\/p>\n\n\n\n<p>The thing I like most about TanStack Start&#8217;s RSC implementation is that it&#8217;s very explicit. You have a clear API for declaring what you want to be rendered as an RSC.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rendering Interactive Content<\/h2>\n\n\n\n<p>We could end this blog post pretty abruptly and simply render our <code>SidePanelTrigger<\/code> component directly from our RSC. That would work fine, so long as we do one thing: place the <code>\"use client\"<\/code> pragma at the top of the file that contains this component. That would work, and we would be done.\u00a0<\/p>\n\n\n\n<p>But instead, let&#8217;s do it a slightly harder way, so we can explore another feature of RSC: passing props. Instead of rendering <code>SidePanelTrigger<\/code> from the RSC, we&#8217;ll\u00a0<strong>pass\u00a0<\/strong>this component to the RSC\u00a0<em>as a prop\u00a0<\/em>and render it from there. While overkill for this use case, it&#8217;ll show off some of the amazing flexibility RSC offers, enabling a single Server Component to render different content via different props passed to the RSC.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Passing Props to our RSC<\/h2>\n\n\n\n<p>Let&#8217;s finish this up. We need some new helpers.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { createCompositeComponent, CompositeComponent } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-start\/rsc\"<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Props in RSC<\/h3>\n\n\n\n<p>It\u2019s important to remember that, by the time our Server Function runs and we return the shell component, our component has&nbsp;<em>already rendered<\/em>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">return<\/span> renderServerComponent(&lt;ApplicationShellEmptyRSC \/&gt;);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p> It&#8217;s done. It rendered <em>on the server<\/em> and the thing we&#8217;re holding, returned from our server function and <code>renderServerComponent<\/code> (or <code>createCompositeComponent<\/code>) is, conceptually, the final markup for the RSC.<\/p>\n\n\n\n<p>We&#8217;ll be putting it in our component tree, but again, and this cannot be overstated, the RSC itself has <em>already rendered<\/em>.<\/p>\n\n\n\n<p>That means, if you think you can just pass some data into the RSC, and use that data to adjust the content that&#8217;s rendered, you fundamentally do not understand how RSCs work: again, by the time you attempt to display it in your component tree, the RSC component has <em>already rendered<\/em> on the server, and produced markup. This makes any attempt to pass props from the client to the RSC to influence said markup a non-sequitur<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">So how can we pass props?<\/h3>\n\n\n\n<p>What RSCs do allow is to pass in <code>children<\/code> content, or other <em>components<\/em> as props. The RSC recognizes these props and renders &#8220;holes&#8221; or &#8220;slots&#8221; (in a generic sense) for them to be dumped into.<\/p>\n\n\n\n<p>Let&#8217;s take a look.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Adding Props to our RSC<\/h3>\n\n\n\n<p>Here&#8217;s our new server function.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> getAppShell = createServerFn({\n  method: <span class=\"hljs-string\">\"GET\"<\/span>,\n}).handler(<span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">return<\/span> createCompositeComponent(\n    (\n      props: PropsWithChildren&lt;{\n        HeaderContent: FC&lt;{ name: <span class=\"hljs-built_in\">string<\/span>; avatar: <span class=\"hljs-built_in\">string<\/span> }&gt;;\n      }&gt;,\n    ) =&gt; &lt;ApplicationShell children={props.children} HeaderContent={props.HeaderContent} \/&gt;,\n  );\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We&#8217;re using <code>createCompositeComponent<\/code> which allows us to declare props. We&#8217;re using the <code>PropsWithChildren<\/code> generic helper, which implicitly declares a children prop of type <code>ReactNode<\/code>, and we&#8217;re adding a <code>HeaderContent<\/code> prop, which is a component.<\/p>\n\n\n\n<p>One neat thing about TanStack&#8217;s RSC implementation is that props passed like this are <em>automatically<\/em> client components; you don&#8217;t have to add <code>\"use client\"<\/code> to the file, although it&#8217;s fine if you do. Note that this applies to components you <em>pass<\/em> to props. Content you <em>render<\/em> as <code>children<\/code> can include RSC content if you&#8217;d like. You&#8217;d render other RSC content exactly like we did above, with <code>{appShell}<\/code>.<\/p>\n\n\n\n<p>As before, we load our RSC in our loader.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">  loader: <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> appShell = <span class=\"hljs-keyword\">await<\/span> getAppShell();\n    <span class=\"hljs-keyword\">return<\/span> { appShell };\n  },<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then grab it in our component.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> { appShell } = Route.useLoaderData();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And now we can render this with the <code>CompositeComponent<\/code> helper. We render <code>CompositeComponent<\/code> like a component, and pass the RSC result as the <code>src<\/code> prop, as well as any other props we may have.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CompositeComponent<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{appShell}<\/span> <span class=\"hljs-attr\">HeaderContent<\/span>=<span class=\"hljs-string\">{SidePanelTrigger}<\/span>&gt;<\/span>\n  {children}\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">CompositeComponent<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Loading Data in RSC<\/h3>\n\n\n\n<p>Now let&#8217;s look at our actual RSC.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { Suspense, <span class=\"hljs-keyword\">type<\/span> FC, <span class=\"hljs-keyword\">type<\/span> PropsWithChildren } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"react\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { SideBarContent } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/SideBarContent\"<\/span>;\n\n<span class=\"hljs-keyword\">type<\/span> ApplicationShellProps = {\n  HeaderContent: FC&lt;{ name: <span class=\"hljs-built_in\">string<\/span>; avatar: <span class=\"hljs-built_in\">string<\/span> }&gt;;\n};\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> ApplicationShell: FC&lt;PropsWithChildren&lt;ApplicationShellProps&gt;&gt; = <span class=\"hljs-function\"><span class=\"hljs-params\">props<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> { children, HeaderContent } = props;\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;main&gt;\n      &lt;header&gt;\n        &lt;Suspense fallback={&lt;span&gt;&lt;<span class=\"hljs-regexp\">\/span&gt;}&gt;\n          &lt;UserHeaderMenu HeaderContent={HeaderContent} \/<\/span>&gt;\n        &lt;<span class=\"hljs-regexp\">\/Suspense&gt;\n        &lt;span&gt;Header&lt;\/<\/span>span&gt;\n      &lt;<span class=\"hljs-regexp\">\/header&gt;\n      &lt;section&gt;\n        &lt;SideBarContent \/<\/span>&gt;\n      &lt;<span class=\"hljs-regexp\">\/section&gt;\n      &lt;section&gt;\n        &lt;div&gt;\n          &lt;section&gt;{children}&lt;\/<\/span>section&gt;\n          &lt;footer&gt;&lt;<span class=\"hljs-regexp\">\/footer&gt;\n        &lt;\/<\/span>div&gt;\n      &lt;<span class=\"hljs-regexp\">\/section&gt;\n    &lt;\/m<\/span>ain&gt;\n  );\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Notice this piece.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">&lt;Suspense fallback={&lt;span&gt;&lt;<span class=\"hljs-regexp\">\/span&gt;}&gt;\n  &lt;UserHeaderMenu HeaderContent={HeaderContent} \/<\/span>&gt;\n&lt;<span class=\"hljs-regexp\">\/Suspense&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We&#8217;re rendering another component, <code>UserHeaderMenu<\/code> within a <code>Suspense<\/code> tag, and passing through the HeaderContent prop, which again is a React client component that takes in a name and an avatar prop. Let&#8217;s see it next<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">UserHeaderMenu<\/span>(<span class=\"hljs-params\">props: { HeaderContent: FC&lt;{ name: <span class=\"hljs-built_in\">string<\/span>; avatar: <span class=\"hljs-built_in\">string<\/span> }&gt; }<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> { HeaderContent } = props;\n\n  <span class=\"hljs-keyword\">await<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Promise<\/span>(<span class=\"hljs-function\"><span class=\"hljs-params\">resolve<\/span> =&gt;<\/span> setTimeout(resolve, <span class=\"hljs-number\">1000<\/span>));\n  <span class=\"hljs-keyword\">const<\/span> avatar = <span class=\"hljs-string\">\"https:\/\/d193qjyckdxivp.cloudfront.net\/avatar.jpg\"<\/span>;\n  <span class=\"hljs-keyword\">const<\/span> name = <span class=\"hljs-string\">\"Adam Rackis\"<\/span>;\n\n  <span class=\"hljs-keyword\">return<\/span> &lt;HeaderContent name={name} avatar={avatar} \/&gt;;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Since we&#8217;re in an RSC, we don&#8217;t have to use the <code>use<\/code> pseudo-hook. We can just <code>await<\/code> our data however we want, and while those data are pending, the Suspense boundary&#8217;s fallback will render without blocking the rest of the content, as before. Then, a second later, our data will be ready, and our avatar will show.<\/p>\n\n\n\n<p>This works and produces the same experience we saw originally, with the client-rendered version, except now as an RSC.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Total Savings with RSC<\/h2>\n\n\n\n<p>What are the savings? The non-RSC version pushed 308KB of JavaScript into the client. The RSC version reduces it to 203KB (both measurements are from production builds).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">When to use RSC<\/h2>\n\n\n\n<p>Please don&#8217;t think this is a panacea, or even something you should use in every project. The larger and more expensive the component tree, the larger your potential savings. But if your component tree isn&#8217;t doing much, isn&#8217;t pulling in heavy dependencies (which don&#8217;t need state or interactivity), doesn&#8217;t have a wide import graph with things that are conditionally rendered, then there&#8217;s a good chance RSC will offer you minimal benefit.<\/p>\n\n\n\n<p>This is a tool like any other, and like any other tool, you need to know when to reach for it, and when not to.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Concluding Thoughts<\/h2>\n\n\n\n<p>TanStack&#8217;s implementation of RSC is what I wanted all along, without ever knowing it. Data fetching in TanStack is already simple; RSC exists to provide a more performant rendering idiom where things don\u2019t exist on the client when they don\u2019t need to, or when existing on the client would be expensive.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>RSCs in TanStack Start are server-only executed code \u2014 perhaps a significant improvement over the Next.js implementation.<\/p>\n","protected":false},"author":21,"featured_media":9703,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[62,435,240],"class_list":["post-9689","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-react","tag-rscs","tag-tanstack"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/05\/RSCs.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/9689","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/21"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=9689"}],"version-history":[{"count":11,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/9689\/revisions"}],"predecessor-version":[{"id":9726,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/9689\/revisions\/9726"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/9703"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=9689"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=9689"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=9689"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}