React Server Components - What’s the fuss about?

17 Sep, 2024

8 min read

rsc-cover

According to popular saying - change is the only constant. Whether it is within a month or 5 years, a change is bound to occur somewhere, and this time it is in the React ecosystem. React introduced in late 2020 the “Zero-Bundle-Size React server components”, but it has only now, within the last 14 months, become practically the hottest topic in React.

In fact it was during my research for this article I found it’s been around for quite a few years. So why is it at the tip of everyone’s tongue now, and what does it even mean for developers and the future of web applications?

I will be answering these questions, a few others that will come up - and oh they will, because this concept is quite the head scratcher -, and also showing quite extensively what you need to understand about RSCs.


RSCs and SSR

In 2023 October, during the nextjs conf, the nextjs community introduced a new app router directory that makes use of RSC. This would be the first support of RSC by a framework and as such, for the first time, give developers a more practical sense of using RSC.

This hit like a bang - boom! Those who did not already know about RSC now knew, and those who did could now easily try it out! So it became one of the hottest topics in the tech space and still is, articles have been rolling out, tweets and spaces as well, on the topic.

If you have heard about RSCs then you very likely have heard about server side rendering - SSR. Now what is all this talk about server on the frontend? You might ask.

Before I go into a technical explanation of what SSR and RSC is, I’d like to touch on the long existing problem.

Before the release of React 19. All we had was Client side rendering for building React applications and web applications in general. *state limitation after explanation


Client side rendering (CSR) is the rendering of the html, and the javascript bundle on the browser(the client) . Practically all the aspects of your app are being rendered on the client.

Typically, React has only one html file, and the rest of your components are JSX or plain JS files. The JSX components get compiled to JS functions. So we can say React applications consist mostly of javascript, and then the single index.html file.

In the index.tsx file we use the createRoot interface from ReactDOM, to render our application components App in the root element, found in the index.html page. This is shown in the code snippets below.

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

reportWebVitals();

The App component is the parent component of the application. It contains the visual and interactive parts of the application. These components have to compile to javascript first, and the browser has to run this javascript bundle.

All of that takes time, and while that is being done, all the browser has available to show in the meantime is that empty html file, so that is what the user sees until the js is ready. Only then does the user see the full content of your application.

This is why in many websites, when you open it for the first time, you see a loading screen (the spinning circle) or better yet a skeleton loader, giving the user a preview or a shell of what is to come, and after a few milliseconds or seconds, the complete webpage shows.

Before the introduction of Javascript, traditional websites were made of HTML contents, which is static content that can be rendered immediately by the browser. see MDN


With client side javascript we can enrich our static websites with interactivity. But it comes at the cost of time. In web performance terms, low TTI and higher LCP - show illustration It also has as a disadvantage the inability to be SEO optimized - we see this all the time but it is very rarely explained how CSR limits SEO.


We see all the time that CSR is not SEO optimized, but it is very rarely explained how CSR limits SEO. The reason is that Search engines make use of web crawlers which are essentially bots that run through your website in an attempt to scrape or extract content needed to rank your websites in their results page.

Not all of the web crawler bots are able to run javascript, and the ones that can may time out while waiting for the browser to download the javascript needed to load the content of your website.

The inability to get the meta content needed to rank your sites is what leads to poor SEO. [support with some kind of illustration]


We’ve built web applications this way for years, bearing these limitations, and doing our best to improve performance, employing methods like lazy loading, (find out what react helmet does).

It was, I would say, long overdue for a change to occur that aims to completely take away these limitations. Fortunately, the React team came through for us with RSC.

Again, before I go into RSCs - I know 😅 I’m not holding out on you, don’t worry, we’ll get to RSCs soon - let us first discuss server side rendering.

Server Side Rendering

Server side rendering is the rendering of a part or all of the web content on the server or on a server run time like nodejs, compiling it into a static html response and sending that response to the client, which the client can immediately read because it is static.

import { renderToString } from 'react-dom/server';

const html = renderToString(<App />);
Using the renderToString method from react-dom/server, we can render our application components on the server. It returns an HTML string of the rendered component. This string can be sent to the client as a response to a request.

The server usually sends back to the client the static html, the javascript bundle (usually a single page application, like React) and styling assets.

The pre-rendering of the web content by the server can be done at request time (upon navigation to a webpage), or at build time - this is known as a static site generation.

With Server side rendering, a considerable amount of time is saved when loading/entering a web page [show illustration] . The user can immediately see a fully loaded web page. But bear in mind that this web page is static - the static html response from the server. To get our website fully responsive and interactive as a SPA should, Hydration comes in.

import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

const domNode = document.getElementById('root');

hydrateRoot(
  domNode,
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
hydrateRoot is used to “attach” React to existing HTML that was already rendered by React in a server environment.

Hydration is like sprinkling our applications with the liquid of interactivity 💦 💦.

During the hydration process, the javascript bundle is loaded, and event handlers are attached to the existing DOM of the application, and the application state is restored. Hydration is done on the client side, by the client (browser).

That is the full picture of SSR [show more illustrations] [also so show code snippets of ssr and hydration]

Finally, to the topic that brought us here - React Server Components!

Understanding RSCs

React server components are components rendered exclusively on an environment separate from the client environment, and even your SSR server. RSCs are rendered way ahead of time, before the project is bundled. The separate environment where RSCs are rendered is the ‘server’ in React server components.

We are able to access server only interfaces like filesystem, and even query databases!

Server components do not get added to the javascript bundle of the project, but are instead compiled, returning an html output which can then be rendered using SSR.

RSCs can be written as async functions. This is because they can perform asynchronous operations like fetching data from an API, querying a database, or reading from the filesystem.

import React from "react";
import { getAllPosts } from "@/app/api/posts";
import OverviewContent from "./overviewContent";

export default async function Blogs() {
  const posts = await getAllPosts();

  return <OverviewContent posts={posts} />;
}
A server component declared as an async function

One thing you’ll notice if you try to make use of browser interactive APIs, and event handlers is that you can not 😬 You will see an error / warning similar to this.

rsc-error

Well, this is because server components are not sent to the client(the browser), but are instead pre-built on the server into an output html which is what gets sent to the browser - and so, they can not be interactive.

It is noteworthy that React hooks also can not be used in a server component. Reason being, hooks cause re-renders, and server components render only one time.

Additionally, all props passed to a client component from a server component must be serializable, like strings, numbers, boolean, and iterables of serializable values, like objects and arrays, etc.

This can be not so great in instances where you have a component that should say, fetch a list of items and perform a DOM action that makes an update to the list . You would like to have the list rendered on the server, and you also like to update a piece of the ui with the items in that list.

/booklist

export default async function BookList() {
  const [newBooks, setNewBooks] = React.useState<BooksProps>([]);
  const [readBooks, setReadBooks] = React.useState<BooksProps>([]);
  React.useEffect(() => {
    fetch(
      `https://yourlistapi`
    ).then((res) => {
      const data = res.json();
      data.then((data) => {
        setNewBooks(data.books);
      });
    });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
   //updates the newBooks state and readBooks state
  };

    return (
    <div>
     <p>New Books</p>
        {newBooks?.map((b) => (
          <div>
            <input
              type="checkbox"
              onChange={handleChange}
            />
            <div>
              <p>{b.title}</p>
            </div>
          </div>
        ))}
        <p>Read Books</p>
        {readBooks?.map((b) => (
          <div>
            <input
              type="checkbox"
              onChange={handleChange}
            />
            <div>
              <p >{b.title}</p>
            </div>
        ))}
      </div>
    </div>
  );

  }, []);
A server component trying to update the DOM with an OnChange function, updating the state of the application

The compiler will throw an error, asking you to mark this component with a 'use client' directive. Thus making it a client component.

If we go ahead and make it a client component, it works, of course - we can interact with it. In the Book collection card below, try checking some of the books as read - but the list of books will not be rendered on the server, and so the user will have to wait for the javascript bundle to be downloaded before they can see the list of books.

You can test this by hitting the refreshing the page. For a more visible effect, you can throttle the network speed to slow 3G in the browser dev tools, and refresh the page. You will see the list of books rendered only after the javascript bundle has been downloaded.

To throttle your internet, follow theses steps

Open Developer Tools (right-click, then select "Inspect").

Got to the "Network" tab

Under the "No throttling" dropdown, select a slower connection (e.g., "Slow 3G").

Now reload the page to observe the delay more clearly.

Books Collection

New Books

Read Books

Fortunately, there is a way to make this sort of thing work.

Making Server Components Interactive

Just like the warning above advises, we can separate the component, and have the piece of server logic on the server component, passing the resulting data as props to a client component.

We can import client components into server components. The server component will render first, instructing the bundler to create a bundle for the client component. And so in the browser, the client component will see the output data of the server component rendered as props.

client components can be called in server components.

Server components on the other hand can not be called in client components.

This is because the server components get rendered first. Calling a client component in a server component will require the server to render twice.

export default async function BookCollectionSSR() {
  let books = [];
  try {
    const res = await fetch(
      `https://yourlistapi`
    );
    const data = await res.json();
    books = data.results?.books
  } catch (e) {
    console.log(e);
  }

  return (
      <BookList nytBooks={books} />
  );
}
The BookList component is the client component which has all the interactivity and state change. It gets called from a server component and recieves as props the books data preloaded on the server

Books Collection

New Books

COUNTING MIRACLES

COUNTING MIRACLES

Nicholas Sparks

INTERMEZZO

INTERMEZZO

Sally Rooney

THE WOMEN

THE WOMEN

Kristin Hannah

HERE ONE MOMENT

HERE ONE MOMENT

Liane Moriarty

Read Books

If you refresh the page you will see the list of books rendered immediately as opposed to what we have with the client component book collection above, and the user can interact with the list of books even before the javascript bundle is downloaded. Try checking some of the books as read to interact with the component.

Another way to make RSCs interactive is by using server actions. You can jump right to where I explain server actions.

RSC VS SSR

If we have SSR, why then do we need RSC?

SSR is focused on the initial rendering of the application. It renders a static HTML file which can not yet be interacted with. Not until the Javascript bundle has been downloaded on the client, and hydration happened. Only then can the application behave like a normal React app, and become interactive.

Whereas, RSCs are not included in the Javascript bundle, only the resulting html is sent to the client. This significantly helps to reduce the bundle size of the application. Also, with the help of “Server actions” Server components can be interacted with even before the Javascript of the application is downloaded.

In essence, RSC and SSR are not opposites, nor does RSC make SSR useless. Rather they are both essential for a highly performant application.

Server components output HTML can even be SSR’d - rendered on the server - for better performance

Server Actions

Server actions are async functions that run on a server run time and can be called from a client component.

"use server";
export async function getAllPosts() {
  const slugs = getPostSlugs();
  const posts = slugs
    .map((slug) =>
      getPostBySlug(slug, [
        "title",
        "publishedDate",
        "description",
        "timeToRead",
        "coverImage",
        "dataURI",
        "tag",
      ])
    )
    // sort posts by date in descending order
    .sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
  return posts;
}
when the "use server" directive is placed at the top of a file, every export in that file is marked as a server action

We stated earlier that server components can not be called from a client component. But server actions can be called from a client component.

To call a server action in a client component, the server action has to be marked with the “use server” directive.

'use client'
import React from "react";
import {  getAllPosts } from "@/app/actions/posts";
export default function BlogSection() {
return(
  return <button onClick={() => getAllPosts()}>Generate post list</button>
)
}
Usage of getAllPosts Server action in a client component

Server actions are useful in form interaction, and can enable users to submit a form even before the browser has downloaded the javascript bundle of the application. With server actions you can calculate a piece of logic or fetch a list of items on the server and render its result on the client where you have access to browser API, hooks etc and can have an interactive component.

Benefits of RSC

I’m sure you can already see how useful server components are. The most obvious benefit is improved performance. By rendering components on the server and sending the HTML output to the client, RSCs help reduce the overall JavaScript bundle size.

This means faster load times and lower time-to-interactive (TTI), as the browser doesn’t have to download and execute as much JavaScript before the user can see and interact with the content. This results in a smoother and more responsive experience for users, especially on slower networks or devices.

Other benefits include:

Better SEO

With RSCs, the HTML is rendered server-side, meaning crawlers and bots that cannot run JavaScript can still access the content of your page. This leads to better SEO outcomes as search engines can more easily index the relevant information right away. In contrast, traditional client-side rendering (CSR) often struggles with SEO due to the reliance on JavaScrip

Server-side Data Fetching and Logic

RSCs allow you to run server-side logic, like querying databases or interacting with the file system, directly within components. This is a huge advantage because it centralizes heavy operations on the server, keeping the client lean and fast. Developers no longer have to handle these tasks on the client or make multiple API calls—RSCs can take care of it before the content is even sent to the client.

Early Interactivity with Server Actions

With server actions, users can interact with a form or trigger an event before the entire JavaScript bundle is downloaded. This leads to more immediate feedback and a better user experience, as interactions can happen even when the app hasn’t fully loaded.

Lower Client-Side Complexity

By keeping non-interactive logic on the server, RSCs reduce the complexity on the client side. With fewer components to hydrate and manage on the client, the app remains simpler and more efficient. This also lessens the need for complex client-side state management, as the server can handle most of the heavy lifting.

Conclusion

React Server Components (RSC) represent a major evolution in the way we approach building web applications, bringing together the best of server-side efficiency and client-side interactivity. By allowing developers to offload heavy, non-interactive logic to the server while keeping essential interactivity on the client, RSCs help significantly reduce bundle sizes, improve performance, and create more seamless user experiences.

While React Server Components don’t make traditional SSR obsolete, they complement it, offering more flexibility in rendering and interactions. With features like server actions, we can now optimize application performance even further by handling crucial interactions server-side before the client JavaScript is fully loaded.

As we look to the future, RSCs open up exciting possibilities for building faster, more responsive applications without compromising on interactivity.

For developers, understanding and adopting RSCs may be the key to building cutting-edge, scalable web apps that can handle the growing demands of modern users.

Have an awesome project idea?

Feel free to reach out to me if you're looking for a developer, have a query, or simply want to connect.

Made by Emmanuella Okorie — Copyright 2024

twitter-iconlinkedin-icon github-icon