DEV Community

Mohammed Amine Belhadi
Mohammed Amine Belhadi

Posted on

React Server Components: How Did We Get Here?

React has gone since it's inception through multiple breaking changes in the way of how it is used. Things like hooks that made class components deprecated, or how it started as a client side library that turned into a Full-stack, server-side rendered framework like NextJS are a few examples that show the ever-evolving nature of React.

The latest hot trends of React now being the React Compiler and Server Components. In this article, we will talk more about Server Components, what are they and how to use them. But before doing that, let's take a walk down the memory lane and see how things used to be in the past.

Traditional Web Applications:

Back in the day, web apps used server side rendering as the way to serve their content. When you request a page, the server process whatever information needed, like user data, spits it into a template and serves it back to you.

SSR

This approach worked for the most part, but there were some issues with it, mainly:

  • Harder to make it interactive. Considering the imperative nature of vanilla JavaScript, you had to explicitly state (pun intended) what you want to do with your app. Stuff like handling stateful values, updating UI and sending form requests all proved to be a pain to handle.
  • Feels sluggish to the user. Websites made with something like React has a mobile app feeling to it, transitions between pages is seamless. If you compare it to these traditional websites, virtually every action will cause a hard reload like switching pages or submitting forms.
  • Servers had to process whatever information needed before serving the HTML as a response to the user, until then, the user will stare into a blank screen waiting. If processing that info takes quite the time, the user experience might not be the best.

So devs had to find a way to make websites more modern, more smooth, more... seamless both to users and devs alike. Hmmm, what could we use?

Enter React:

React introduces a different way of developing UI, rather than the imperative way of vanilla JS, it uses a declarative one through JSX.
By using React, you will get:

  • An easier time updating the DOM on state changes.
  • Introducing interactivity would be a breeze.
  • Components make it trivial to reuse common UIs across your app.
  • Since route changes will be handled by JavaScript and not having to serve a new html file for each page, the transitions will be seamless and feels closer to a native mobile app.

This rendering strategy is called Client-Side Rendering, or CSR. And apps made by React or a similar framework are called SPAs, short for Single Page Applications. Because you are serving one empty html file for every request and JS handles showing the relevant content based on the route.

Buuuuuut with almost everything in life, this sounds too good to be true, right? Surely there is some drawbacks to using this library. Well, by using React, there are some cons to be caused, mainly:

  • The CSR strategy will result in users doing the heavy work of rendering the website. This will cause low end devices such as old phones to feel laggy.
  • This will also affect SEO since the content of the html file will be an empty div. Search engines are getting better and better at reading the content of SPAs, but I prefer raw html over JavaScript when it comes to SEO.
  • And most importantly, unnecessary server roundtrips.

A server roundtrip is the request response cycle, when you request a resource from a server, you'll get a response, like an HTML or a JSON. The time it took since you started the request to the moment you received the response is a roundtrip. Now roundtrips are needed to get and send information between you, the user, and the server. But it is best practice to minimize them for better performance.

When using React, you are initially requesting an html file with a script tag and an empty div. Then the browser will request the JavaScript from that tag. Next is the JavaScript painting the page with content, only at this point will user see something, prior to this only a blank page to stare at.

Look at this simple React component:

import React from 'react'
import ReactDOM from 'react-dom/client'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

function App() {
    return <h1>Hello World</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Upon inspecting the network tab using DevTool, we will find a bunch of requests made to the server:
Network tab of a simple React app
Ignoring the files coming from Vite, we will end up with two requests:
1- First request being the document (named localhost).
2- Second being the main.tsx file that is requested after the document is retrieved.

When main.tsx is received, the React code is invoked and takes over, rendering the content of the page. If you view the page source (CTRL+U on Chrome), you'll see that the body contains an empty div.

<!doctype html>
<html lang="en">
  <head>
    <script type="module">
import RefreshRuntime from "/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>

    <script type="module" src="/@vite/client"></script>

    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React Test</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now this doesn't sound too bad, only two requests is trivial and nothing to worry about. But in most real world projects, you are probably going to communicate with an API to retrieve and send info. Let's update our App component a little bit:

    function App() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => { // Bad code btw, use react-query
        const fetchUsers = async () => {
            try {
                const response = await fetch(
                    "https://jsonplaceholder.typicode.com/users"
                );
                const data = await response.json();
                setUsers(data);
            } catch (err) {
                setError(err.message);
            } finally {
                setLoading(false);
            }
        };
        fetchUsers();
    }, []);

    if (loading) return <p>Loading...</p>;
    if (error) return <p>Error: {error}</p>;

    return (
        <div>
            <h1>Users List</h1>
            <ul>
                {users.map((user) => (
                    <li key={user.id}>
                        {user.name} - {user.email}
                    </li>
                ))}
            </ul>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Now let's take a look once again in the Network tab and see what changed:
Network tab after updating code
There are two new requests at the end called users. These are the fetch request we added in the App component, notice how they are the last to be called.

Note: There are two users fetch call, this is not a bug, this is an intended feature caused by React.StrictMode that makes your useEffect run twice in your code to ensure it is bugfree. I recommend this video for more info.

Another Note: I am using useEffect to invoke the fetch request to make the code simple. Don't use it in production and instead use a library like react-query. Read this article if you want to know why this is a bad idea.

A step by step procedure of how websites made using pure React work to render the website goes something like this:

SPA

Of course these steps will probably take a few seconds to finish so it is nothing big, but considering the fact that users are impatient and might leave the website if it takes more than 3 seconds to load, I think it is best we focus more on our site performance for better conversion rates.

We need a way that makes the initial HTML file renders the content prior to giving it back to the user. For that reason, several frameworks like Nextjs were created. Such frameworks work by running the React code on the server then serves the resulting content as html along with JS to the user. The JavaScript code's job is to hydrate the content and make it interactive as well as initiating any API requests.

Thus, by using Nextjs, we skip right to the point where we get some content on receiving the response from the server (Better First Contentful Paint) rather than an empty html. But can we do better? Can we do something about these API requests that only happen once the JS is downloaded and invoked?

Introducing getServerSideProps:

This is a function whose purpose is to run any code on the server side, usually to initiate fetch requests and return their result to the page as prop before serving it to the user.

By using getServerSideProps, we now removed the extra API calls initiation in step 6 and moved it back to the server. But overall, this approach has two problems:

  1. The getServerSideProps function works only on a page level, you can't use it inside components.
  2. The JS hydration works on ALL elements, surely there are some that don't require any interactivity like article links for example. If we can remove the JS from parts that don't need it, we will significantly reduce the bundle size that users have to download.

So we need a way to run server side code on components that returns only HTML content without any unneeded JS code.

React Server Components (RSCs):
Server Components, to put it simply, are the same components you write in regular React. The only difference is that they produce an HTML output without any JavaScript in it. This limits these components' interactivity, such as hooks not being usable, in exchange for a lower JavaScript bundle size.

Often, there is a confusion between React Server Components (RSCs) and Server-Side Rendering (SSR), with people thinking they are the same. A good way to differentiate between them is that in SSR, the components' code runs on both the server and the client, while RSCs run only on the server.

Spin up a Nextjs server (I am using 14.2) and modify the Home component as such:

import { useState } from "react";

export default function Home() {
    const [state, setState] = useState();
    return <h1>Hello World</h1>;
}
Enter fullscreen mode Exit fullscreen mode

You'll get this error:

You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
│ Learn more: https://nextjs.org/docs/getting-started/react-essentials

Next.js by default marks all components as RSCs unless you use the 'use client' directive. If you need to use something like hooks, you must opt into client components by using that directive.
Let's dive into the powers of Server components, imagine you are building an ecommerce website, in the PLP you usually have the product info section as well as the product reviews among other components. Let's focus on these two:

export default async function Product() {
    return (
        <>
            <ProductInfo />
            <ProductReviews />
        </>
    );
}

async function ProductInfo() {
    const product = await fetchProductInfo();
    return (
        <div>
            <h1>Product Name: {product.name}</h1>
            <p>Product Description: {product.description}</p>
            <p>Price: ${product.price}</p>
            <button>Buy</button>
        </div>
    );
}

async function ProductReviews() {
    const reviews = await fetchProductReviews();

    return (
        <div>
            <h2>Reviews</h2>
            {reviews.map((review) => {
                return (
                    <div key={review.id}>
                        <strong>{review.user}</strong>
                        <p>{review.reviewText}</p>
                    </div>
                );
            })}
        </div>
    );
}

function fetchProductInfo() {
    return new Promise((resolve) => {
        setTimeout(() => {
            const product = {
                name: "T-Shirt",
                description:
                    "This Stylish T-Shirt is made from high-quality cotton, offering both comfort and style. Perfect for casual wear, it comes in various sizes and colors to suit your preference.",
                price: 25,
            };
            resolve(product);
        }, 500);
    });
}

function fetchProductReviews() {
    return new Promise((resolve) => {
        setTimeout(() => {
            const reviews = [
                { id: 1, user: "User 1", reviewText: "Review 1" },
                { id: 2, user: "User 2", reviewText: "Review 2" },
                { id: 3, user: "User 3", reviewText: "Review 3" },
            ];
            resolve(reviews);
        }, 500);
    });
}
Enter fullscreen mode Exit fullscreen mode

To fetch data inside an RSC, the component must be async. Here, we are simulating two fetch requests to get data about the product. Notice that we are not using any loading states as we typically do in traditional client components. This is because Server Components run the fetch on the server, and the user only sees the result of that fetch.

If you've been carefully reading through this article, you might remember that one of the drawbacks I mentioned about traditional SSR is that the user needs to wait for any necessary processing before they receive the HTML. Try modifying the seconds parameter in the setTimeout to 2000ms for one of the fetch functions.

You'll notice that the entire page keeps loading until the longer setTimeout is completed, and only then do we get a response. This is not ideal, especially in this example. On ecommerce websites, most users are more interested in the product information rather than the product reviews. If the product reviews take a long time to load, users will have to wait for something they don't need right away, or might not need at all.

React Suspense:

With the usage of RSCs and Streaming, we can display parts of the UI immediately while deferring others to load later. This way, we can mark content that is important to users and prioritize loading it faster than other, less important content.

For example, if fetching product reviews takes longer, like two seconds on average, we can defer fetching the reviews since we know most users won't be interested in them initially. This allows us to prioritize and wait only for the more important fetchProductInfo request, ensuring that critical product information is displayed quickly.

To defer components, we can leverage Suspense to display a loading UI that is later replaced by the result of the fetch inside the Suspensed component. Let's see this in action:

export default async function Product() {
    return (
        <>
            <ProductInfo />
            <Suspense fallback={<p>Loading</p>}>
                <ProductReviews />
            </Suspense>
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

This way, we can ensure that no matter how many seconds the fetch for reviews takes, the user will still receive a chunk of the UI, hopefully the one they are most interested in, while the reviews are deferred for later.

The good thing about Server Components is that they are, well, components. You are not strictly forced to use them in all parts of your application. You can use them where it makes sense, while opting for client components in areas where they are more appropriate. This gives you the freedom to write whatever code fits best for your specific use case.

That's it folks, I think that RSCs is a significant advancement in the React world by presenting an efficient solution to rendering UI on the server. With the ability to defer slow and unneeded components, RSCs enable us to enhance user experience by delivering content faster and more efficiently, meeting users' needs with greater speed. However, it is important to note that this new rendering pattern is still relatively new, thus using it in production may introduce some risks, proceed with caution.

Further reading:

Making Sense of React Server Components
Server Components

Top comments (0)