Daniel Saewitz

Server Components Give You Optionality

It feels as though people see React Server Components as a prescription, in that they prescribe how you must use them. In reality, they are largely additive: you don't have to fetch your data on the server and you don't have to run a server at all. What React Server Components do is give you optionality to decide where you want your code to run: the server, the client, or both. And when: build-time or run-time. It gives you the ability to move between those environments seamlessly through composition. For experts who want freedom of choice, RSCs are a powerful solution. If you find them confusing, it's because nothing like them has existed before, but once you internalize their design, you'll see why their ideas will live on for a long time.

With traditional client-side rendered (CSR) apps: your code only runs on the client. That is trading off some UX for a more elementary DX, generally used for dashboard style apps. With SSR (isomorphic/universal)1 your code runs on the server & the client - meaning the same code runs on both2. With RSCs you have full access to the prior two paradigms, yet we add one more: your code can run on the server only (or without a server at build-time). That doesn't seem like a big distinction, but it actually has major ramifications.

CSR

Let's start with a basic CSR app - just a simple vite react app. It's a dashboard, so we don't need SEO. Our page loads with some static html and the javascript bundle. We show the user our app's logo and then React loads in and takes over. Simple, easy, and just works.

Here's some code to render our navbar.

1export function NavBar() {
2 return (
3 <header>
4 <Link href="/">Home</Link>
5 <Link href="/about">About</Link>
6 <Link href="/world-domination">World Domination</Link>
7 <Link href="/secrets">Secrets</Link>
8 </header>
9 );
10}

This works great, our app is reasonably solid. No complaints right? But as your app grows, so does your bundle–and so does your component tree. Things start to get beefy quickly. Client-side javascript is slow. I'm not talking about bytes, I'm talking about parsing and execution. Because with CSR ALL of our components end up having to run on the client, even those that will never change.

Now, what if we want the user to see our content on page load right away, rather than our company logo? This <NavBar /> is static - so why not pre-render it? That's objectively better UX right? I'm not talking about developer experience, just user experience.

Let's just try React SSR. If your reaction is: "no really, I don't want a server" - just bear with me, we'll get back to you I promise.

SSR (universal/isomorphic)

Okay, so we want server rendering, not for SEO but so the user sees the page pre-rendered when they load the URL. We make our components render on the server and the client. The code above is actually unchanged. It renders on the server and spits out html/js and includes it in the client bundle. Easy.

Let's add some data fetching – what if we wanted to render a Post in our app?

1export function Post({ id }) {
2 const { data, loading } = useQuery(fetchPost(id));
3
4 if (loading) {
5 return <Skeleton />;
6 }
7
8 return (
9 <div>{data.title}</div>
10 );
11}

Seems reasonable - and simple. Again, our code runs on both the server and the client. So useQuery bundles up the data we fetched on the server, sends it down to the client, hydrates the client data cache and when <Post /> is re-rendered, it skips the fetch because we already have the data. A bit more complex, but still working great and no extra fetches.

But rSSR code has to be "universal" - meaning it has to render on the server and the client. What is the server if the same code must run on the client? Well then the server is just another headless client, right? We can't access node apis, we can't hide api keys, and we can't do anything special that only the server can do. It may take a second to marinate, but the side-effects of that idea are large.

What would happen if we had the ability to run some code only on the server?

RSC

Now the server is independent, standing tall. On request, we render the <Post /> component, fetch its data and render it. This component's code is not sent to the client, just the resulting HTML/JSX. The component's lifecycle dies on the server, sending only its ashes3 (jsx) to the client.

1export async function Post({ id }) {
2 const data = await fetchPost(id);
3
4 return (
5 <div>{data.title}</div>
6 );
7}

When you want to enter rSSR via RSC, you just denote 'use client' and compose in your "client component". You're right there, back to the client bundle. And when you want client-only code, you can call React.lazy. You have all the options available to you.

By having the ability to render components only on the server, you unlock the full power of that environment: you can await a fetch request, you can render markdown without needing a client-side renderer, or build a complex svg graph via d3 without sending the library to the client. If you like Rails or PHP, you'll find a lot of similarities here, but the benefits don't stop there.

What you'll find is that your pages get far more rigid, durable, and less prone to breakage. Dates don't have to re-render, impure functions don't break (Math.random()), and there's no slippage when the client tries to take over. Your pages will render faster and leaner–you'll still have some client javascript, but you'll be shedding a whole lot of it.

I told you I don't want a server

I know. I'm glad you're still here. I promised we'd get back to you. Okay, fine, you don't want a server–that's okay. What if it was still worthwhile to adopt RSCs?

Let's export our next.js app directory as a static bundle4: No Server.

If we render the <NavBar /> as a server component, what actually reaches the client? Just the html/jsx – it gets rendered at build-time and its lifecycle dies there. Fewer code lifecycles means less potential for things to break, less computation, and fewer bytes to send over the wire. Again, we end up with a much leaner website.

Think about this component: it's effectively dead code. In CSR, it will continue to render over and over as its parents re-render, but it's doing nothing. We could slap a React.Memo on it, or we could just skip that whole headache and never send it to the client at all. Way simpler and easier to reason about.

When you want to write your client-side code, you can do so by just denoting a single 'use client' at the base of the interactive tree. In this model, we build up islands of interactivity around our static shell .. sound familiar? Yes, this pattern is effectively the design of astro, but you maintain all of the benefits of the React ecosystem natively, such as client-side routing and componentization.

With Vite CSR or SSR - you still send every component to the client. It's all or nothing. It might not seem like a lot, but it adds up quickly. Wouldn't it be nice to choose? As a React team member pointed out, RSCs are akin to tree-shaking your code by design, rather than having the bundler do the guess-work for you.

Optionality

In my experience, what we as developers crave is choice. Some pages you want to render statically, some pages you want access to the raw server, and some pages you want to do a little bit of both. RSCs are the only tool that give you full optionality to each of those, while maintaining composition and componentization. And if you don't want a server, you can still gain the astro-like benefits of adopting RSCs, reducing client-side churn at little cost.

But that's not to say RSCs come at zero cost. They require rethinking how we structure our websites–and for many, the cost of that confusion may not be worth adopting them today. Their tooling and build-systems are more fraught than a simple vite app. I believe for now, RSCs are a tool for the frontend professional. They are an expert swiss-army knife that gives us fine-tuned control. If you don't see why you might want that power then you may not need it. But if you do, you'll find them to be a natural conclusion–and I think if you are writing complex websites on a daily basis, you'll eventually want that flexibility. RSCs give you the building blocks to make your websites more durable, rigid, and performant, whether you choose to use a server or not.

RSCs give you optionality to choose which environments you want your code to run in (Server, Server+Client, or Client) and when. They offer you, the developer, the autonomy to wield that choice, navigating between them through composition rather than hard-stops (consider PHP->jQuery, or Rails->React). These concepts will go on to live in other frameworks–not a question of if–just when. Perhaps through simpler mental models. I guess we'll have to wait and see. Other frameworks have come up with ways to run server-only code, but they tend to lose componentization or composition. React maintains both, no easy feat. It doesn't tell you what you must do, it gives you the choice to pick what you want to do. And with those choices come optionality-not prescription. Don't you want to have that choice?