Introduction
So, here’s something that makes me feel old: React celebrated its 10th birthday this year!
In the decade since React was first introduced to a bewildered dev community, it’s gone through several evolutions. The React team has not been shy when it comes to radical changes: if they discover a better solution to a problem, they’ll run with it.
A couple of months ago, the React team unveiled React Server Components, the latest paradigm shift. For the first time ever, React components can run exclusively on the server.
There’s been so much friggin’ confusion about this online. Lots of folks have lots of questions around what this is, how it works, what the benefits are, and how it fits together with things like Server Side Rendering.
I’ve been doing a lot of experimentation with React Server Components, and I’ve answered a lot of my own questions. I have to admit, I’m way more excited about this stuff than I expected to be. It’s really cool!
So, my goal today is to help demystify this stuff for you, to answer a lot of the questions you might have about React Server Components!
To put React Server Components in context, it’s helpful to understand how Server Side Rendering (SSR) works. If you’re already familiar with SSR, feel free to skip to the next heading!
Originally, React was designed to work exclusively in-browser, on the user’s device. The user would receive an HTML file that looked like this:
That bundle.js
script includes everything we need to mount and run the application, including React, other third-party dependencies, and all of the code we’ve written.
Once the JS has been downloaded and parsed, React springs into action, conjuring all of the DOM nodes for our entire application, and housing it in that empty
The problem with this approach is that it takes time to do all of that work. And while it’s all happening, the user is staring at a blank white screen. This problem tends to get worse over time: every new feature we ship adds more kilobytes to our JavaScript bundle, prolonging the amount of time that the user has to sit and wait.
Server Side Rendering was designed to improve this experience. Instead of sending an empty HTML file, the server will render our application to generate the actual HTML. The user receives a fully-formed HTML document.
That HTML file will still include the tag, since we still need React to run on the client, to handle any interactivity. But we configure React to work a little bit differently in-browser: instead of conjuring all of the DOM nodes from scratch, it instead adopts the existing HTML. This process is known as hydration.
I like the way React core team member Dan Abramov explains this:
Hydration is like watering the “dry” HTML with the “water” of interactivity and event handlers.
Once the JS bundle has been downloaded, React will quickly run through our entire application, building up a virtual sketch of the UI, and “fitting” it to the real DOM, attaching event handlers, firing off any effects, and so on.
And so, that's SSR in a nutshell. A server generates the initial HTML so that users don't have to stare at an empty white page while the JS bundles are downloaded and parsed. Client-side React then picks up where server-side React left off, adopting the DOM and sprinkling in the interactivity.
Let's talk about data-fetching in React. Typically, we've had two separate applications that communicate over the network:
-
A client-side React app
-
A server-side REST API
Using something like React Query or SWR or Apollo, the client would make a network request to the back-end, which would then grab the data from the database and send it back over the network.
We can visualize this flow using a graph:
This first graph shows the flow using a Client Side Rendering (CSR) strategy. It starts with the client receiving an HTML file. This file doesn't have any content, but it does have one or more tags.
Once the JS has been downloaded and parsed, our React app will boot up, creating a bunch of DOM nodes and populating the UI. At first, though, we don't have any of the actual data, so we can only render the shell (the header, the footer, the general layout) with a loading state.
You've probably seen this sort of thing a lot. For example, Airbnb starts by rendering a shell while it fetches the data it needs to populate the actual listings:
The user will see this loading state until the network request resolves and React re-renders, replacing the loading UI with the real content.
Let's look at another way we could architect this. This next graph keeps the same general data-fetching pattern, but uses Server Side Rendering instead of Client Side Rendering:
In this new flow, we perform the first render on the server. This means that the user receives an HTML file that isn't totally empty.
This is an improvement — a shell is better than a blank white page — but ultimately, it doesn't really move the needle in a significant way. The user isn't visiting our app to see a loading screen, they're visiting to see the content (rental listings, search results, messages, whatever).
To really get a sense of the differences in user experience, let's add some web performance metrics to our graphs. Toggle between these two flows, and notice what happens to the flags:
Each of these flags represents a commonly-used web performance metric. Here's the breakdown:
-
First Paint — The user is no longer staring at a blank white screen. The general layout has been rendered, but the content is still missing. This is sometimes called FCP (First Contentful Paint).
-
Page Interactive — React has been downloaded, and our application has been rendered/hydrated. Interactive elements are now fully responsive. This is sometimes called TTI (Time To Interactive).
-
Content Paint — The page now includes the stuff the user cares about. We've pulled the data from the database and rendered it in the UI. This is sometimes called LCP (Largest Contentful Paint).
By doing the initial render on the server, we're able to get that initial “shell” drawn more quickly. This can make the loading experience feel a bit faster, since it provides a sense of progress, that things are happening.
And, in some situations, this will be a meaningful improvement. For example, maybe the user is only waiting for the header to load so that they can click a navigation link.
But doesn't this flow feel a bit silly? When I look at the SSR graph, I can't help but notice that the request starts on the server. Instead of requiring a second round-trip network request, why don't we do the database work during that initial request?
In order words, why not do something like this?
Instead of bouncing back and forth between the client and server, we do our database query as part of the initial request, sending the fully-populated UI straight to the user.
But hm, how exactly would we do this?
In order for this to work, we'd need to be able to give React a chunk of code that it runs exclusively on the server, to do the database query. But that hasn't been an option with React… even with Server Side Rendering, all of our components render on both the server and the client.
The ecosystem has come up with lots of solutions to this problem. Meta-frameworks? like Next.js and Gatsby have created their own way to run code exclusively on the server.
For example, here's what this looked like using Next.js (using the legacy “Pages” router):
Let's break this down: when the server receives a request, the getServerSideProps
function is called. It returns a props
object. Those props are then funneled into the component, which is rendered first on the server, and then hydrated on the client.
The clever thing here is that getServerSideProps
doesn't re-run on the client. In fact, this function isn't even included in our JavaScript bundles!
This approach was super ahead of its time. Honestly, it's pretty friggin’ great. But there are some downsides with this:
-
This strategy only works at the route level, for components at the very top of the tree. We can't do this in any component.
-
Each meta-framework came up with its own approach. Next.js has one approach, Gatsby has another, Remix has yet another. It hasn't been standardized.
-
All of our React components will always hydrate on the client, even when there's no need for them to do so.
For years, the React team has been quietly tinkering on this problem, trying to come up with an official way to solve this problem. Their solution is called React Server Components.
At a high level, React Server Components is the name for a brand-new paradigm. In this new world, we can create components that run exclusively on the server. This allows us to do things like write database queries right inside our React components!
Here's a quick example of a “Server Component”:
As someone who has been using React for many years, this code looked absolutely wild to me at first. 😅
“But wait!”, my instincts screamed. “Function components can't be asynchronous! And we're not allowed to have side effects directly in the render like that!”
The key thing to understand is this: Server Components never re-render. They run once on the server to generate the UI. It's sent to the client and locked in place. As far as React is concerned, this output is immutable, and will never change.
This means that a big chunk of React's API is incompatible with Server Components. For example, we can't use state, because state can change, but Server Components can't re-render. And we can't use effects because effects only run after the render, on the client, and Server Components never make it to the client.
It also means that a lot of the old rules don't apply. For example, in traditional React, we need to put side effects inside a useEffect
callback or an event handler or something, so that they don't repeat on every render. But if the component only runs once, we don't have to worry about that! We can put the side effects wherever we want.
Server Components themselves are surprisingly straightforward, but the “React Server Components” paradigm is significantly more complex. This is because we still have regular ol’ components, and the way they fit together can be pretty confusing.
In this new paradigm, the “traditional” React components we're familiar with are called Client Components. I'll be honest, I don't love this name. 😅
The name “Client Component” implies that these components only render on the client, but that's not actually true. Client Components render on both the client and the server.
I know that all this terminology is pretty confusing, so here's how I'd summarize it:
-
React Server Components is the name for this new paradigm.
-
In this new paradigm, the “standard” React components we know and love have been rebranded as Client Components. It's a new name for an old thing.
-
This new paradigm introduces a new type of component, Server Components. These new components render exclusively on the server. Their code isn't included in the JS bundle, and so they never hydrate or re-render.
So, typically, when a new React feature comes out, we can start using it in our existing projects by bumping our React dependency to the latest version. A quick npm install react@latest
and we're off to the races.
Unfortunately, React Server Components doesn't work like that.
My understanding is that React Server Components needs to be tightly integrated with a bunch of stuff outside of React, things like the bundler, the server, and the router.
As I write this, there's only one way to start using React Server Components, and that's with Next.js 13.4+, using their brand-new re-architected “App Router”.
Hopefully in the future, more React-based frameworks will start to incorporate React Server Components. It feels awkward that a core React feature is only available in one particular tool! The React docs has a “Bleeding-edge frameworks” section where they list the frameworks that support React Server Components; I plan on checking this page from time to time, to see if any new options become available.
In this new “React Server Components” paradigm, all components are assumed to be Server Components by default. We have to “opt in” for Client Components.
We do this by specifying a brand-new directive:
That standalone string at the top, "use client"
, is how we signal to React that the component(s) in this file are Client Components, that they should be included in our JS bundles so that they can re-render on the client.
This might seem like an incredibly odd way to specify the type of component we're creating, but there is a precedent for this sort of thing: the "use strict" directive that opts into “Strict Mode” in JavaScript.
We don't need to specify the "use server"
directive in our Server Components; in the React Server Components paradigm, components are treated as Server Components by default.
One of the first questions I had when I was getting familiar with React Server Components was this: what happens when the props change?
For example, suppose we had a Server Component like this:
Let's suppose that in the initial Server Side Render, hits
was equal to 0
. This component, then, will produce the following markup:
But what happens if the value of hits
changes? Suppose it's a state variable, and it changes from 0
to 1
. HitCounter
would need to re-render, but it can't re-render, because it's a Server Component!
The thing is, Server Components don't really make sense in isolation. We have to zoom out, to take a more holistic view, to consider the structure of our application.
Let's say we have the following component tree:
If all of these components are Server Components, then it all makes sense. None of the props will ever change, because none of the components will ever re-render.
But let's suppose that Article
component owns the hits
state variable. In order to use state, we need to convert it to a Client Component:
Do you see the issue here? When Article
re-renders, any owned components will also re-render, including HitCounter
and Discussion
. If these are Server Components, though, they can't re-render.
In order to prevent this impossible situation, the React team added a rule: Client Components can only render other Client Components. When we convert a component to a Client Component, it automatically converts its descendants.
One of the biggest “ah-ha” moments I had with React Server Components was the realization that this new paradigm is all about creating client/server boundaries. Here's what winds up happening, in practice:
When we add the "use client"
directive to the Article
component, we create a “client boundary”. All of the components within this boundary are implicitly converted to Client Components. Even though components like HitCounter
don't have the "use client"
directive, they'll still hydrate/render on the client in this particular situation.
Let's look at this at a bit of a lower level. When we use a Server Component, what does the output look like? What actually gets generated?
Let's start with a super-simple React application:
In the React Server Components paradigm, all components are Server Components by default. Since we haven't explicitly marked this component as a Client Component (or rendered it within a client boundary), it'll only render on the server.
When we visit this app in the browser, we'll receive an HTML document which looks something like this:
We see that our HTML document includes the UI generated by our React application, the “Hello world!” paragraph. This is thanks to Server Side Rendering, and isn't directly attributable to React Server Components.
Below that, we have a tag that loads up our JS bundle. This bundle includes the dependencies like React, as well as any Client Components used in our application. And since our
Homepage
component is a Server Component, the code for that component is not included in this bundle.
Finally, we have a second tag with some inline JS:
This is the really interesting bit. Essentially, what we're doing here is telling React “Hey, so I know you're missing the Homepage
component code, but don't worry: here's what it rendered”.
Typically, when React hydrates on the client, it speed-renders all of the components, building up a virtual representation of the application. It can't do that for Server Components, because the code isn't included in the JS bundle.
And so, we include the virtual representation that was generated on the server. When React loads on the client, it re-uses that description instead of re-generating it.
If you're curious to see true representations of how Server Components are serialized and sent over the network, check out the RSC Devtools by developer Alvar Lagerlöf.
React Server Components is the first “official” way to run server-exclusive code in React. As I mentioned earlier, though, this isn't really a new thing in the broader React ecosystem; we've been able to run server-exclusive code in Next.js since 2016!
The big difference is that we've never before had a way to run server-exclusive code inside our components.
The most obvious benefit is performance. If we can keep half of our components as Server Components, it means that our JS bundles will get significantly lighter. This means that our applications will become interactive more quickly:
This is maybe the least exciting thing to me, though. Honestly, most Next.js apps are already fast enough when it comes to “Page Interactive” timing.
If you follow semantic HTML principles, most of your app should work even before React has hydrated. Links can be followed, forms can be submitted, accordions can be expanded and collapsed (using
). For most projects, it's fine if it takes a few seconds for React to hydrate.
But here's something I find really cool: we no longer have to make the same compromises, in terms of features vs. bundle size!
For example, most technical blogs require some sort of syntax highlighting library. On this blog, I use Prism. The code snippets look like this:
A proper syntax-highlighting library, with support for all popular programming languages, would be several megabytes, far too large to stick in a JS bundle. As a result, we have to make compromises, trimming out languages and features that aren't mission-critical.
But, suppose we do the syntax highlighting in a Server Component. In that case, none of the library code would actually be included in our JS bundles. As a result, we wouldn't have to make any compromises, we could use all of the bells and whistles.
This is the big idea behind Bright, a modern syntax-highlighting package designed to work with React Server Components.
This is the sort of thing that gets me excited about React Server Components. Things that would be too cost-prohibitive to include in a JS bundle can now run on the server for free, adding zero kilobytes to our bundles, and producing an even better user experience.
It's not just about performance and UX either. After working with RSC for a while, I've come to really appreciate how easy-breezy Server Components are. We never have to worry about dependency arrays, stale closures, memoization, or any of the other complex stuff caused by things changing.
Ultimately, it's still very early days. React Server Components only emerged from beta a couple of months ago! I'm really excited to see how things evolve over the next couple of years, as the community continues to innovate new solutions like Bright, taking advantage of this new paradigm. It's an exciting time to be a React developer!
React Server Components is an exciting development, but it's actually only one part of the “Modern React” puzzle.
Things get really interesting when we combine React Server Components with Suspense and the new Streaming SSR architecture. It allows us to do wild stuff like this:
It's beyond the scope of this tutorial, but you can learn more about this architecture on Github. Also, we explore all of this fancy modern stuff in my soon-to-be-released course, The Joy of React!
The Joy of React is a beginner-friendly interactive course, designed to help you build an intuition for how React works. We start at the very beginning (no prior React experience required), and work our way through some of the most notoriously-tricky aspects of React.
This course has been my full-time focus for almost two years now, and it includes all of the most important stuff I've learned about React in over 8 years of experience.
You'll even learn how to do next-level layout animations like this, using Framer Motion:
This course releases next week, on September 13th. You can learn much more about the course here:
React Server Components is a significant paradigm shift. Personally, I'm super keen to see how things develop over the next couple of years, as the ecosystem builds more tools like Bright that takes advantage of Server Components.
I have the feeling that building in React is about to get even cooler. 😄
Last Updated
September 6th, 2023