React Server Components: The Future of UI
React Server Components are going to improve the way we build web applications in a huge way... Once we nail the abstractions...


Did you know React Router is adding React Server Components support? It's still experimental, but it's very close to landing, and I think React Router's take on RSC is really great. Here's what you need to know.
The first step is to enable RSC in your React Router app. You'll need to install two plugins:
@react-router/dev/vite@vitejs/plugin-rscHere's how to update your vite.config.ts:
import { reactRouter, // unstable_reactRouterRSC as reactRouterRSC,} from '@react-router/dev/vite'import tailwindcss from '@tailwindcss/vite'// import rsc from '@vitejs/plugin-rsc'import { defineConfig } from 'vite'import devtoolsJson from 'vite-plugin-devtools-json'import tsconfigPaths from 'vite-tsconfig-paths'export default defineConfig({ server: { port: process.env.PORT ? Number(process.env.PORT) : undefined, }, plugins: [ tailwindcss(), tsconfigPaths(), // Replace reactRouter() with: // reactRouterRSC(), // rsc(), devtoolsJson(), ],})
So much of what makes RSC work is the integration with the bundler. A lot of the heavy lifting comes from the Vite team, with significant contributions from the React Router team.
Once you've enabled RSC, you'll also need to remove the Scripts component from
your root layout. With RSC, those scripts are included as part of the RSC
payload automatically:
import { isRouteErrorResponse, Links, Meta, Outlet, // Remove this Scripts import // Scripts, ScrollRestoration,} from 'react-router'// ... other code ...export function Layout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> </head> <body> {children} <ScrollRestoration /> {/* Remove this Scripts element */} {/* <Scripts /> */} </body> </html> )}
Now that RSC is enabled, what can you do with it? One powerful pattern is returning UI from your loaders instead of just data.
Here's a traditional loader that returns data:
export async function loader() { const movies = await getMovies() return { movies }}export default function MoviesPage({ loaderData }: Route.ComponentProps) { const { movies } = loaderData const moviesUI = movies.map((movie) => ( <MovieCard key={movie.id} movie={movie} /> )) return ( <main> {/* ... */} <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> {moviesUI} </div> </main> )}
The problem with this approach is that we're sending data to the client that we don't actually need once it's been rendered. We're hydrating data unnecessarily.
With RSC, you can return the completed UI from your loader:
export async function loader() { const movies = await getMovies() const moviesUI = movies.map((movie) => ( <MovieCard key={movie.id} movie={movie} /> )) return { moviesUI }}export default function MoviesPage({ loaderData }: Route.ComponentProps) { const { moviesUI } = loaderData return ( <main> {/* ... */} <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> {moviesUI} </div> </main> )}
Now you're just serving UI from the server without sending any of the underlying data. This is especially powerful if you're pulling data from a CMS where you don't know what UI you need to render before you have the data. Instead of dynamically loading components (leading to spinners) or loading all possible components upfront, you can make that determination in the loader and send the resulting UI, making your payload way smaller.
But wait—if the entire page could be a server component, why use a loader at all? You can make the entire route a server component:
import { MovieCard } from '#app/movie-card.tsx'import { getMovies } from '#app/movies-data.ts'export async function ServerComponent() { const movies = await getMovies() const moviesUI = movies.map((movie) => ( <MovieCard key={movie.id} movie={movie} /> )) return ( <main className="bg-background min-h-screen"> {/* ... */} <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> {moviesUI} </div> </main> )}
Notice that we're exporting a named ServerComponent function instead of a
default export. This means we don't need the loader at all—we can directly get
the data inside the component itself because it's a server component.
The entire UI is rendered on the server, sent to the client, and not hydrated because there's no interactivity. This makes types simpler too—no need for loader data types.
One of the most powerful aspects of React Router's RSC implementation is that you can migrate incrementally. If you have nested routes, a child route can be a client route, and that can have a child that's a server route. They don't have to know about each other.
This means if you're working on a big application with different teams in charge of different routes, one team can decide to make their portion an RSC route without the rest of the app having to change. This makes incremental migration really possible and really powerful.
React Router has always had actions, but React now has form actions as a primitive. With RSC, you can use React's form actions directly with server functions.
Here's how you can create a server function:
'use server'export async function setIsFavorite(formData: FormData) { // Simulate API call delay await new Promise((resolve) => setTimeout(resolve, 50)) const movieId = Number(formData.get('id')) const isFavorite = formData.get('isFavorite') === 'true' // Update the movie's favorite status const movie = movies.find((m) => m.id === movieId) if (movie) { movie.isFavorite = isFavorite }}
The 'use server' directive says that the things exported from this module can
be referenced in the client. When the form action is called, it makes an RPC
call to the server to actually call this function.
Then you can use it directly in your form:
<form action={setIsFavorite}> <input type="hidden" name="id" value={movie.id} /> <input type="hidden" name="isFavorite" value={String(!movie.isFavorite)} /> <button type="submit"> {movie.isFavorite ? 'Favorite' : 'Not Favorite'} </button></form>
Why is this cool? With React Router, all actions and loaders are tied to a route. But if you use React's form actions with server functions, they can be tied to a component. You could take this form, make it a component all by itself, and reuse it all over the place—on the details page, on cards, in a chat feature, anywhere.
The status quo for doing this with React Router before RSC was having to find the closest route for all of those and have an action on every one of those routes. With RSC, you don't have to do that. A component can manage its own data loading and data mutations. You could even publish it on npm, and anybody who supports RSC would be able to use your component.
Of course, sometimes you need interactivity. That's where client components come
in. If you're using hooks like useState or useEffect, you need to mark that
component as a client component:
'use client'import { Activity, useEffect, useState } from 'react'import { type Movie } from '#app/movies-data.ts'export function MovieTrailer({ movie }: { movie: Movie }) { const [showTrailer, setShowTrailer] = useState(false) // ... rest of the component}
The 'use client' directive says that this code needs to go to the browser
because it has state or other client-side logic. It still server renders, but it
also sends that code to the client to make it interactive.
The really cool thing is that you can use client components inside server components without changing anything about the server component. The server component doesn't need to know that the client component exists—it just works.
RSCs in loaders are really helpful if you're building something like a big timeline that has many, many combinations of different components that could possibly be used, and you don't know which one until you have the data. That's perfect for RSCs in loaders and RSC routes.
For everybody else, it's a nice handy thing. I should call out that sometimes the combination between data and template together will actually be bigger than just data and template separate—it kind of depends on the scenario. But for most people, just using RSCs and RSC routes makes a lot of sense.
The cool thing about React Router is that you don't have to completely switch over. You can migrate incrementally as you see that being a really beneficial thing. Aside from the fact that you can also do your data loading right in the components, so types are a lot better, and you can use Suspense boundaries and all of that stuff as well.
Server functions are super useful for those of you who have components that need to have data loading and data mutation associated to them. And now you can just put those all over the place.
Client components, of course, are only really useful if you are using server components. If you're not turning your routes or different components into server components, you're not going to be using client components. Your entire app is still client components. But if you decide RSC is really the way that you want to go, then client components are the way you want to go.
One more thing I should mention: you can actually still do a static build of a React Router app with Server Components. That is part of the design of React Server Components. You should be able to still get some of the benefits of RSC with being able to deploy this to a static environment. You don't necessarily need a server to use React Server Components—the server can be the build server.
React Router's implementation of React Server Components is really well done. It supports:
'use server''use client'All of these are supported in React Router, and the incremental migration path makes it really powerful for teams working on large applications. For the use cases that React Server Components serves really well, I think this is really good.
Delivered straight to your inbox.
React Server Components are going to improve the way we build web applications in a huge way... Once we nail the abstractions...

Learn how to generate unique IDs for client-only data in React using the Web Crypto API, ensuring stable keys and accessible, dynamic UI components.

If you use a ref in your effect callback, shouldn't it be included in the dependencies? Why refs are a special exception to the rule!

How web app architecture evolved from full page reloads to React Server Components, and why each step in this journey mattered for developers and users.
