React Server Components Deep Dive

6 min read
React Server Components Deep Dive

TL;DR

  • Server Components run entirely on the server. That's why they can directly access databases or backend logic inside the component. React renders the component on the server and sends static HTML to the browser — the client never sees the actual component code.

  • Tradeoffs: No useState, useEffect, or DOM APIs (they’re static).

  • The new paradigm: All components are server components by default. To make a client component, add "use client" at the top of the file. It creates a client boundary, and all its children become client components.

To understand why server components are created, you need to first understand how React was rendered.

Client-side rendering (CSR)

Back when create-react-app still existed, React was client-side rendered.

When we visit a webpage made by a React app, these happen:

The CSR flow

  1. The browser sends a request to the server.
  2. The server returns two things:
    • An almost empty HTML file with a script tag linking to a bundle.js file.
    • A gigantic bundle file that contains all the JavaScript of the app.

Something like this:

<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <script src="/static/js/bundle.js"></script>
  </body>
</html>
  1. The browser parses the JavaScript file to create the initial UI. But the data that you need to fetch from the server is not there yet. These components are most likely in the loading state.
  2. Browser fetches those from the servers.
  3. Finally, with all the resources present, a web page is rendered.

Client side rendering graph

We call this client-side rendering (CSR)

Problems

The problem with this is the page load speed. It takes time to parse the JavaScript in the client to create a meaningful UI. Before that, the user will be staring at an empty white screen and is very likely to already leave the website to continue doom scrolling.

Plus, it’s not good for SEO.

So people came up with another strategy.

Server-side rendering (SSR)

See, in this process, there are actually two main parts.

Client side rendering graph

1758598993275_anya-like-this

First is to create DOM nodes and the UI. Second is adding interactivity, like event handlers and states. What if we separate them?

The SSR flow

  1. Instead of creating the UI in the browser, we first do it on the server, then send the static UI back.
  2. The browser parses the JavaScript file and adds interactivity (event handlers, states) to the prerendered HTML. This process is called hydration.
  3. The server data are still not there yet. So we still need to get it from other servers.
  4. The final content is rendered.

server side rendering graph

Problems

This process is slightly better because users see a richer UI sooner. Plus, you can download less JavaScript.

But if you look at the graph and are a little more critical of why things are done this way.

You might wonder, since we create the UI and query the database on the server, why can't we do them together? And you can do fewer network requests.

1758603881304_rsc-flow

That wasn't possible with the previous React components.

So they created the server component.

What is React Server Components (RSC)

Server components are created entirely in the server.

Because of that, you can do server operations in a React Server component, like fetching data from a database, or accessing the file system.

export default async function Page() {
  const users = await getUsersFromDB(); // This function runs on the server

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Which is absolutely wild. Before this, you had to maybe do a fetch in useEffect and then create an API endpoint in the backend to handle these kinds of requests. But with server components, you can skip these steps.

import { useState, useEffect } from "react";

function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    async function fetchUsers() {
      const response = await fetch("/api/users");
      const data = await response.json();
      setUsers(data);
    }
    fetchUsers();
  }, []);

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Server:

export default async function handler(req, res) {
  const users = await getUsersFromDB(); // This function runs on the server
  res.status(200).json(users);
}

Under the hood, React will run the database query, use the data to create a static HTML file, and send it to the browser.

The client never sees the component, just the static HTML.

Limitations of server components

But because the component is rendered on the server, it can't use any React hooks like useState or useEffect. It also can't access any browser APIs like the DOM or local storage.

For that, we still need client components.

Server component vs client component

Now we have two kinds of components:

  1. The original component we are using that are rendered in the browser, which is called client component
  2. This server component

Here is a table summarizing the differences between React Server Components and Client Components:

Server Components (SC)Client Components (CC)
Execution EnvironmentRendered on the server onlyRendered in both the client & the server
RerenderRendered once per requestRe-rendered as necessary
JavaScript BundleNot included in client bundle, reduces bundle sizeIncluded in client bundle, increases bundle size
Access to React HooksNoYes
Access to Browser APIsNoYes
async/await componentYesNo
InteractivityStaticFully interactive

When to use server components

Use server components when you want a faster page load, reduced JavaScript bundle size and SEO. Use client components when you can't use a server components, like adding interactivity or event handlers to the UI.

The new paradigm

With these two components in mind, React has a completely new paradigm, which is called RSC (aka react server component, I know it's a little bit confusing but I didn't come up with the name).

Under this new paradigm, every React component is a server component by default, unless you specify it to be a client component with a "use client" directive.

"use client";

function Page() {
  const [text, setText] = useState("lorem");

  return <p>{text}</p>;
}

When you make a component a client component, all its children will become client components. We call this the client boundary.

1758644558677_client-boundary

Are React server components worth it?

React server components completely change the way we write React apps. If you come from previous versions, the extra complexity doesn't seem to be worth it at first. But with the benefit of reduced bundle size, better performance and simplified data fetching, it's definitely worth a try. At least, I can't imagine going back to the old way of doing things.

Level up your react, one step at a time.

Get the written notes delivered to your email inbox to learn something new about react one at a time.