Hands-on with React Server Components

Unlike server-side rendering, React Server Components aim to fully replace client-side functionality with work done on the server. Let’s see how this works.

Hands-on with React Server Components
SeventyFour / Shutterstock

React remains a flagship among front-end JavaScript frameworks, and the React team continues to pursue avenues to keep it relevant. One of the more important developments on the roadmap is React Server Components. 

React Server Components provide a means of offloading the work behind a component to the server. This avoids having to ship bundled JavaScript and having to serve secondary API requests to hydrate the component.

React Server Components are a preview feature that can be enabled in React 18. 

Why use React Server Components

Before we look at how React Server Components will work, let’s think about why.  First, it’s useful to note that React Server Components are distinct from server-side rendering (SSR). As the RFC from the React team states,

[SSR and Server Components are] complementary. SSR is primarily a technique to quickly display a non-interactive version of client components. You still need to pay the cost of downloading, parsing, and executing those Client Components after the initial HTML is loaded.

So in contrast to SSR, where we are aiming to render out an initial version of a component which then behaves as a normal client-side animal, React Server Components intend to fully replace the client-side functionality with work done on the server. This has two main benefits:

  1. Bundled JavaScript doesn’t need to be shipped over the wire to the client. The JavaScript is imported and executed, and the results are consumed, on the server.
  2. Initial Ajax/API requests are not required to hydrate the component. The component can interact directly with back-end services to fulfill these needs.  This makes for a less chatty client and avoids the “waterfall of requests” sometimes seen as the browser fulfills interrelated data fetches.

Limitations of React Server Components

Because React Server Components are executed in the context of a server-side environment, they have certain limitations, or let’s call them characteristics. Because while these characteristics may be limitations in some ways, they also help us understand why React Server Components are useful.

The main limitations set out by the spec, as compared with normal client-side components:

  • No use of state (for example, useState() is not supported). Why? Because the component is run once and the results streamed to the client; i.e., the component is not running on the client maintaining state.
  • No lifecycle events like useEffect(). Again, because the component is not executing within the browser where it could take advantage of events and side effects.
  • No browser-only APIs such as the DOM, unless you polyfill them on the server. Think in particular of the fetch API where server-side rendering engines commonly provide a polyfill so the server-side functionality looks just like the browser with respect to API calls.
  • No custom hooks that depend on state or effects, or utility functions that depend on browser-only APIs. These are just fallout from the proceeding limits.

Capabilities that React Server Components support that are not supported by client-side components:

  • Use of server-only data sources such as databases, internal services, and file systems. In short, the component has full access to the node environment in which it lives.
  • Use of server hooks. Access to server-side capabilities like the file system can be wrapped in hooks to share functionality with the same spirit as normal hooks.
  • Ability to render other server-side components, native elements (div, span, etc.), and client-side components. 

Keep that last one in mind. React Server Components exist in a hierarchical tree of components that mixes both server components and client components, nested within each other.

Also note that React Server Components in no way supplant other parts of the React ecosystem. In particular, React Server Components don’t replace normal client components. Rather, they enhance client components by allowing you to interject server-only components where appropriate into the tree.

Further, React Server Components can still pass props to their child client components. That means you can intelligently divide your app into interactive sections, handled by client components, and containing server components that load their state entirely from the back end ahead of time.

Using React Server Components

Since there are two kinds of components now, you distinguish them by using server.js and client.js (and other related extensions like server.jsx and client.jsx) for server components and client components, respectively. Notice that client.js components are not something new. They are exactly like the React components you were already familiar with, only they now have a file extension so the engine knows which are which.

If you look at the demo app created by the React team, you’ll see files in the /src directory intermingled and depending upon one another. For example, there are NoteList.server.js and a SideBarNote.client.js files. 

Take a look at the NoteList.server.js source in Listing 1.

Listing 1. NoteList.server.js

import {fetch} from 'react-fetch';

import {db} from './db.server';
import SidebarNote from './SidebarNote';

export default function NoteList({searchText}) {

  // const notes = fetch('http://localhost:4000/notes').json();
  // WARNING: This is for demo purposes only.
  // We don't encourage this in real apps. There are far safer ways to access data in a real application!
  const notes = db.query(
    `select * from notes where title i like $1 order by id desc`,
    ['%' + searchText + '%']
  ).rows;

  // Now let's see how the Suspense boundary above lets us not block on this.
  // fetch('http://localhost:4000/sleep/3000');

  return notes.length > 0 ? (
    <ul className="notes-list">
      {notes.map((note) => (
        <li key={note.id}>
          <SidebarNote note={note} />
        </li>
      ))}
    </ul>
  ) : (
    <div className="notes-empty">
      {searchText
        ? `Couldn't find any notes titled "${searchText}".`
        : 'No notes created yet!'}{' '}
    </div>
  );
}

Several things are illustrated here. First, notice the polyfill for the fetch API on line 1, provided by react-fetch. Again, this let’s you write API requests that look just like client components.

Second, observe how the datastore is accessed via a unified API (the import of db).  This is a convention offered by the React team; you could theoretically hit the database via a typical node API. In any event, the notes variable is populated by directly hitting the database, where the code is commented with warnings to not do this in real life (it is vulnerable to SQL injection).

Third, notice how the body of the view template is typical JSX defined by the function return. 

Fourth and finally, see how the SideBarNote component is imported just like any other component, even though it is a client-side component defined in the SideBarNote.client.js file.

No-bundle components

One of the most compelling things about a React Server Component is that the JavaScript upon which the component depends — those third-party bundles that are imported — do not have to be shipped to the client. They are imported, interpreted, and made us of entirely on the server. Only the result is sent.

For instance, if you look at the Note.server.js you’ll see that it imports a data formatting utility (via import {format} from 'date-fns';). Instead of zipping and shipping, then unzipping and executing, everything happens server-side. You also avoid the ugly alternative of rolling your own data formatter (yuck).

Improved code splitting

Another area where you will potentially see performance and simplicity wins is in code splitting. This is because the server component can tell at run time what code path is executing and make a decision at that time about what code to incorporate.

This is similar to using React.lazy() to import code, except the splitting occurs without intervention. Plus, the server component can begin loading the necessary code path earlier than a client component that must wait until the decision path is loaded and executed. 

The example cited by the RFC is in Listing 2, which is worth taking a quick look at. 

Listing 2. Lazy loading in a server component

import React from 'react';

// one of these will start loading *once rendered and streamed to the client*:
import OldPhotoRenderer from './OldPhotoRenderer.client.js';
import NewPhotoRenderer from './NewPhotoRenderer.client.js';

function Photo(props) {
  // Switch on feature flags, logged in/out, type of content, etc:
  if (FeatureFlags.useNewPhotoRenderer) {
    return <NewPhotoRenderer {...props} />;
  } else {
    return <OldPhotoRenderer {...props} />;
  }
}

In Listing 2, we are making a decision about which component to load (OldPhotoRenderer or NewPhotoRenderer) based on a flag (FeatureFlags.useNewPhotoRenderer). If this were done with React.lazy, the component here would have to be evaluated on the browser before the necessary choice loaded lazily. Instead, with a server component, no use of Lazy is required, and as soon as this code executes on the server, the correct code path will begin loading lazily.

How React Server Components works

Consider the following quote from the React Server Components RFC:

Server Components are rendered progressively and incrementally stream rendered units of the UI to the client. Combined with Suspense, this allows developers to craft intentional loading states and quickly show important content while waiting for the remainder of a page to load.

Interesting. So React Server Components are not actually doing something like SSR, whereby the component is rendered on the server and reduced to HTML and a minimum of JS to bootstrap itself on the client. Instead, the framework is actually streaming the distilled UI state as soon as it is ready.

Imagine that the server encounters the need to render a server component. As soon as the render is ready, the results are marshalled into a compact format that immediately begins streaming to the client.

This means, as the RFC quote points out, that essential elements of the UI can be identified and rendered ASAP. Meanwhile, Suspense can be used to intelligently handle interactive client-side portions.

React meets server

React Server Components represent a bold move for such a popular, corporate-backed, JavaScript project. It clearly states to the world that React and its team are committed to participating in the ongoing bustle of innovation among JavaScript frameworks. 

Not content to sit on their laurels, the React team are working on the same questions that other innovators from Svelte to Qwik to Solid (and Marko and Astro) are thinking about.

Read more about JavaScript development:

Copyright © 2022 IDG Communications, Inc.