When to use React Server Components

Published on

Note: this post assumes some familiarity with React (the notion of components, in particular), and web development in general (usage of client-side JavaScript, communication between client and server, awareness of the DOM).

There's been a lot of buzz lately in the React community about server components. They came on the scene in 2022, have been production-ready since May 2023 (with the Next.js app directory stable release). But what determines when to use a server component vs. a client component?

To answer this, it helps to understand what React server components are, and what problems they're trying to solve. Let's back up a bit and start with...

The key players

When a user interacts with a web site, there are four key players in the communication chain:

The User
The one using the web site. Usually a human, but sometimes an automation system like Selenium.
The Client
The machine with the browser that displays HTML and CSS, and runs JavaScript. Examples: laptop, mobile phone.
The Server
The machine assembling everything needed to display a web page, and sending it to the client.
The Database
Where the data for the web site is stored. Examples of website data: a band's schedule, your bank account balance.

Breaking the chain: not allowed

In this communication chain, each player may only talk to its immediate neighbors. Choose a pair below for details on how they communicate (or don't communicate).

Select a pair of players

Where the React code runs

Because of the unbreakable communication chain, it matters where the React code runs. If the code runs on the client, there's no way to query the database directly (instead, it needs to run an API call via JavaScript to get the data from the server). And if the React code runs on the server, the client

DOM
can't respond to user input via JavaScript; the page can only update if the server sends an entirely new page.

React and Interactivity

React components are, well, reactive. That is, they can morph and change in response to user input or changing conditions, by way of re-rendering. Furthermore, the re-renders occur without needing to load another page from the server. This is the magic of any JavaScript that runs on the client (not just React).

But, as established above, the server can't run interactive JavaScript in response to user input. So any JavaScript that updates the DOM needs to run on the client.

It's worth taking a moment to consider what features require JavaScript to run on the client, and what types don't.

Dynamic Content Quiz

Dynamic Content Quiz

5 questions

Take a look at these examples, and determine whether they need to run JavaScript on the client.

Why not both?

Ok, so the components that need interactive JavaScript to run on the client can't be server components (since server components can't run JavaScript on the client). And client components can't make database calls, only API calls.

At this point, you may be thinking to yourself, I'd like to be able to access the database directly in my React components, and I want to use interactive JavaScript. Can't I do both?

Yes, you can! You'll need server-side rendering (aka

SSR
) which is different from React Server Components (we'll get to
RSCs
in a moment).

Here's the SSR sequence of events:

  1. The server queries the database and receives data (in this example, a list of dragon habitats).
  2. The server renders the React component, with the dragon habitats passed as props. At this point we have a static server component that's incapable of any interactive JavaScript on the client.
  3. The client receives both the rendered server component and the React code.
  4. The React code runs on the client, and the resulting render is used to hydrate the component. Hydration adds interactive JavaScript to the static rendered server component.

SSR in code

Here's what SSR might look like using the Next.js pages router:

// getHabitatsFromDb is a function kept in a different file
// that makes a database call to retrieve the data
import { getHabitatsFromDb } from "../../database-actions";

// Habitat is an interactive component to display a dragon habitat
import Habitat from "../Habitat"

// getServerSideProps is a Next.js function that can pass props
// along from an async call on the server
// https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props
export const getServerSideProps = (async (context) => {
const habitats = await getHabitatsFromDb();
return { props: { habitats } }
});

// Next.js takes care of passing `habitats` as a prop,
// using the return value of getServerSideProps
export default function DragonHabitats({ habitats }) {
return (
<>
<h1>Dragon Habitats</h1>
{habitats.map((h) => {
<Habitat key={h.id} data={h}>
})}
</>
);
}

SSR: The best of both worlds

SSR has the advantages of both a server render and a client render:

  • The client doesn't need an API call to get the dragon habitats; that data comes along with the static server component.
  • Hydration allows interactive JavaScript and dynamic content on the client.

SSR: The worst of both worlds

SSR also has all the disadvantages of both a server render and a client render. Namely the server sends the JavaScript to render the entire component (including all of its children), and then runs all of that JavaScript on the client. Imagine only a small part of the component is interactive this is a lot of unnecessary bundle size and work on the client.

Enter: React server components

React server components (RSCs) allow us to choose: each component can be a server component or a client component (as opposed to SSR, where each component is a server component and a client component).

This allows us to be very surgical about where to use each kind of component, and apply the advantages of each where needed.

React server component benefits

To summarize the benefits of RSCs:

Benefit 1: simpler code

This is similar to an advantage of SSR, but the code is even simpler.

Since the component runs only on the server, the component code can contact the database (or read from the server filesystem) directly (whaaaat?). The client never runs the code, so there's no danger in breaking the communications chain.

Look at how much simpler the server component code is than the client component code when it comes to accessing the database.

A note on syntax: Next.js assumes all components are server components, unless the component starts with the "use client"; directive at the top of the file. That directive designates the component as a client component to Next.js.

Client component:

// the 'use client' directive indicates to Next.js
// that this is a client component
"use client";

import Habitats from "./Habitats";

function DragonHabitats() {
const [habitats, setHabitats] = useState([]);
useEffect(() => {
fetch("/api/habitats")
.then(data => data.json())
.then(setHabitats)
}, []);

// display habitat components
return (
<>
<h1>Dragon Habitats</h1>
{habitats.map((h) => {
<Habitat key={h.id} data={h}>
})}
</>
);
}

Server component:

// Note: no 'use client' directive, which means
// this is a server component as far as Next.js
// is concerned.
import { getHabitatsFromDb } from "../../database-actions";
import Habitats from "./Habitats";

async function DragonHabitats() {
const habitats = await getHabitatsFromDb();

// display habitat components
return (
<>
<h1>Dragon Habitats</h1>
{habitats.map((h) => {
<Habitat key={h.id} data={h}>
})}
</>
);
}

It takes the client component six lines and two hooks to get the data, whereas the server component only needs one line. Notice that server components can be asynchronous as well; they can wait for async functions to return before rendering on the server.

Benefit 2: increased website efficiency

Server components only render on the server, which is generally more powerful than the client. This has two advantages:

  1. Any computationally-intensive rendering will be faster on the server than it would be on the client.
  2. There's no need to send a large JavaScript packages to the client on how to render complex, static components which saves on bundle size.

This Vercel blog post sums up the advantages quite well:

[T]he server, far more powerful and physically closer to your data sources, deals with compute-intensive rendering and ships to the client just the interactive pieces of code.

Understanding React Server Components (Vercel blog post)

React server component examples

The blog you're reading now uses a couple of React server components that illustrate the benefits.

Bright syntax highlighter

I'm using Bright for the code snippets on this blog. The package has tons of features, and a package size to match (38 kB). However, none of that JavaScript makes it to the client. All of the code snippets are rendered on the server, and the server sends only the final rendered HTML.

MDX Renderer

This blog is written in

MDX
and rendered using next-mdx-remote, a library with specific support for React server components. The blog app reads the MDX file right from the server filesystem, and converts the content to HTML without needing to send any MDX (or JavaScript) to your browser. Rendering this blog post on the server saves about 243 kB in bundle size, and the client's not stuck with the work of converting the MDX into HTML.

There are some interactive parts of this blog which can't be server components (like the "communications chain" component). I'll get to the "server parent → client child" pattern in the next section.

When this blog post was written, the next-mdx-remote docs were a bit out-of-date. If you follow the link for React server components docs (above), you may see it describes the Next app router as unstable but that hasn't been the case since May 2023.

When to use React server components

React server components shine when a component needs server resources. This might include direct access to the database or filesystem, or server computing power to render a complicated component. React server components also save on bundle size and client processing, since the components come to the client pre-rendered. Server components won't work if the UI involves interactivity or the component has state that changes and causes a

re-render
for those you'll need a client component. Otherwise, a server component is the more efficient choice.

Why not both? (revisited)

But what if your page needs some of each? You need access to the filesystem, and you want to use server power to render static parts of the page but you also need some interactivity. Guess what: the page you're reading right now fits that description.

There's a common pattern where the parent component is a React server component (in this case, my BlogPost component, which reads the MDX from the filesystem and renders it into HTML). The parent RSC then contains child client components for interactive functionality. Here, BlogPost is a parent to the interactive client components CommunicationsChain and BlogQuiz.

This pattern could also be used on an e-commerce site. Say there's a product listing page, with the products laid out in a grid, like this:

product listings for four dragon toy products, each with an image and product information below the image

The product information comes from the database but there are interactive components too. Mousing over each product reveals a "Quick look" button, which brings up a modal with more product information. Sometimes mousing over the image changes the product view as well. Plus there's a heart button for adding the product to your favorites, which goes from outline to filled when clicked.

the same image as above, but the dragon costume image is different, there's a 'quick look' button over the bottom of the image, and the heart button for the dragon costume is now filled in blue, instead of having a blue outline like before

The code for the product listing component might look something like this:

import getProductDataFromDb from "../../database-actions";

// ProductImage is a client component that includes
// the interactive "quick look" on mouseover, the
// additional image to show on mouseover, and the
// heart-shaped "favorite" button
import ProductImage from "./ProductImage";

// params gets the product ID from the URL
// Example URL path: /products/123456
// where 123456 is the product ID.
// Setting up the routing details is outside
// the scope of this post.
async function ServerComponent({ params }) {
const { productId } = params;
// this is a server component, which can run an
// async function that contacts the database
const productData = await getProductDataFromDb(productId);

return (
<div>
{/* the child client component handles
all of the interactivity */}
<ProductImage data={productData} />
{/* static product data is fine in the server component */}
<h2>{productData.name}</h2>
<p>{productData.description}</p>
{/* ... more static product data here ... */}
</div>
);
}

React Server Component Quiz

All right, time to see how confident you feel about when to use React server components!

React Server Component Quiz

7 questions

Which would you use in each of these situations? Server component? Client component? Or both (the server parent → client child pattern)?

Further reading

Some suggestions for where to go from here:

  1. This post omitted a major advantage of React server components: streaming and Suspense. See the Next.js docs for how server components can use streaming to deliver data as it becomes available even after initial page load. Next.js also uses Suspense to indicate that data's coming.
  2. Understanding React Server Components is an excellent introductory blog post from Vercel.
  3. Josh W. Comeau's blog post Making Sense of React Server Components features useful graphics to visualize the sequence of events for Server components vs client components.
  4. Demystifying React Server Components with NextJS 13 App Router digs deeper into the technical details, with helpful examples.

But wait, there’s more!

Sign up for the Hands-on Web Dev newsletter for weekly web dev challenges and occasional updates on the course series.

Or you can visit the HoWD challenges site to view the challenges and explanations directly.