React Server Components in React Router – First Impressions
07-08-2025A couple of weeks ago, the Remix team shared more insights into how React Server Components (RSCs) will work with React Router. And after a bit of patience, we finally get to experiment with RSCs in React Router (yes 🎉!).
Assuming you're already somewhat familiar with RSCs and what they do, let’s ask the obvious question:
Why would we want this?
Advantages
- RSCs render on the server only (it's quite literally in the name). The only thing that gets sent (streamed) to the client is some serialized data that represents the component. Since the component code never gets sent to the browser, we can also use some "heavier" dependencies like database clients without it impacting your client-side JS bundle.
- Reducing latency, especially in optimizing the time it takes for content to appear for users.
- You can use backend resources directly inside components without exposing them to the client. This enables you to create a tighter backend integration.
How about RSCs in React Router?
⚠️ Note: Everything is still unstable!
We had to be a little patient… but following the RSC docs, we get a glimpse of what the future might look like. So I thought, why not give it a shot and try moving this website over to a React Router with RSCs app?
Initial impressions
While scanning through the example template, I noticed the following changes. Most of these APIs and implementation details will probably change again, but I reckon it's still valuable to see what's there right now.
- React Router itself is no longer used as a Vite plugin. Instead,
@vitejs/plugin-rsc/plugin
is used with 3 entry files. - Instead of
entry.server.tsx
andentry.client.tsx
, we now have:entry.browser.tsx
Hydrate the generated HTML + support post-hydration server actions.entry.rsc.tsx
Will generate the RSC payloadsentry.ssr.tsx
Responsible for handling the incoming request and generating HTML with fetched RSC payloads.
- A different (?) routes configuration with
RSCRouteConfig
. Truth be told: I haven't actually tried playing around with the@react-router/dev/routes
helper functions here.
Migrating blog to RSC
Blogs are a great candidate for Server Component routes since they’re static by nature and I don’t require interactivity at this moment in time.
I made a start here, and (unsurprisingly) the Vercel deployment failed, since it’s still using the old build setup. I just wanted to get it working locally.
The migration went relatively smoothly. I updated the Vite config, added the new entry points, converted the homepage to an RSC route, and made sure everything rendered as expected.
The only part I’m a bit unsure about is the entire <ClientLayout>
component being used here.
Me, being slightly ignorant (and curious), moved everything to the main root
route and only set up a "use client"
component for the root error boundary. So far, it works, but I’m still trying to figure out whether that layout component is necessary or just redundant.
Loader vs RSC
If you've been working with React Router / Remix for the past few years already, the concept of loader
shouldn't be something new to you. You'd export a server loader
in your Route module, fetch some data and return it.
export async function loader({ params }: Route.LoaderArgs) {
const post = await db.getPost(params.id);
return post;
}
export default function Post({ loaderData }: Route.ComponentProps) {
const { title, description } = loaderData;
return (
<article>
<h1>{title}</h1>
<p>{description}</p>
</article>
)
}
This approach will work for years to come but you might also consider a RSC approach where you'd fetch the data from your component directly.
export default async function Post() {
const post = await db.getPost(params.id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.description}</p>
</article>
)
}
To be entirely honest, I wouldn't be able to tell you what the actual difference is. It'll generate the same output, but it's obviously an entirely different way of dealing with fetching data.
Since RSCs can be async, we can also stream (slower) RSCs to the client!
// movies/page.tsx
export default async function MoviePage() {
const lotr = movieApi.fetchTheLordOfTheRingsExtendedTrilogy();
return (
<>
<Suspense fallback={<ABeautifulLoader />}>
<MovieClientPage movie={lotr} />
</Suspense>
</>
)
}
// movies/page.client.tsx
"use client"
export function MovieClientPage({ movie }) {
const movieData = use(movie);
return <MovieDetail data={movieData} />;
}
If you're curious about how the example above works. Go to the React docs to read more about this :) !
Performance
Just for fun I decided to run
autocannon -c 50 -d 30 http://localhost:5173
on the homepage with a RSC route and the OG Route module that's just returning the homepage (basically static html). Nothing too fancy, and probably an absolutely worthless test but the funny thing was that the RSC route would always be able to process more requests.
Should we draw conclusions from this? Yes move everything over now!! (just kidding)
When implementing RSCs just be mindful of the potential performance issues you can introduce. It's a powerful tool but since it's so different from what we're used to.. here are some things you can keep in mind.
- Use Suspense boundaries where needed, RSCs WILL "block" your navigation until the server component is done executing.
- n+1 queries. You create a "list" component where each child RSC fetches data? Not good.
TL;DR
React Router now has experimental support for React Server Components. I tried moving a small part of this project over. Setup was fairly straightforward, and I got it working locally quite quickly.
If you’re feeling adventurous, definitely give it a spin locally! Just expect stuff to break :)