Just a month ago, the React team posted on their blog about the new RFC. Let’s figure out what it is and why it is needed.

What is it

As the name implies, React Server Components are components that run on the server. We now have several kinds of components:

  • Client components
  • Server components
  • Hybrid components

Client components

These are the components that we are writing now, using state and effects, they allow interaction with the user and are executed in the browser. Up to this point, we only had client components. Client components now have a postfix in their names.client.js

Server components

These are the components that are executed on the server, they cannot use state and effects (they are prohibited from using useStateany other “client” hooks). They can be re-requested during the execution of the application. Have access to all server infrastructure. Such components have a postfix in their names .server.js. Also, it cannot pass functions as parameters, only data.

Hybrid components

These components can be executed both on the server and on the client. They have the strongest limitations. They cannot use client hooks and server infrastructure. In fact, they can only contain JSX markup.

Difference with SSR

Having read up to this point, many of you will ask yourself, what is the difference with SSRand like tools Next.js. After all, react was able to run on the server before, right? Not certainly in that way. The difference lies in what SSRreturns us the HTMLmarkup, which then needs to be hydrogenated. It is most often SSRused for the first download of an application so that the user does not see a white screen. In this case, Server Components returns the JSONstructure of the part virtual dom.

The picture is an example of what is transmitted over the network when using server components.

At the moment, nothing has been said about combining SSR and server components, but I think in the future these two approaches can be combined.

Usage example

The picture above shows how the component tree looks like. Previously, it always consisted of client components, now the tree can contain server components.

With the environment ready, to start using the server components, you need to create a file with the postfix .server.js

// Note.server.js - Server Component

import db from 'db.server'; 
// (A1) We import from NoteEditor.client.js - a Client Component.
import NoteEditor from 'NoteEditor.client';

function Note(props) {
  const {id, isEditing} = props;
  // (B) Can directly access server data sources during render, e.g. databases
  const note = db.posts.get(id);

  return (
    <div>
      <h1>{note.title}</h1>
      <section>{note.body}</section>
      {/* (A2) Dynamically render the editor only if necessary */}
      {isEditing 
        ? <NoteEditor note={note} />
        : null
      }
    </div>
  );
}

So we have created the first server component.

Why do you need

When new features are added, it’s cool, but they should have a practical use, this is what the react command describes:

Zero-Bundle-Size Components

We all love dependencies in our application, but sometimes there are so many of them that the application’s volume counts for tens of megabytes. Since we get access to execution on the server, now our dependencies can also perform their functions on the server, without transferring the source code to the client. Imagine you are writing applications to display markdown text, now you would write like this:

// NoteWithMarkdown.js
// NOTE: *before* Server Components

import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}

This adds an additional 74kb to the bundle size, but if we transform this component with server components, we get the following:

// NoteWithMarkdown.server.js - Server Component === zero bundle size

import marked from 'marked'; // zero bundle size
import sanitizeHtml from 'sanitize-html'; // zero bundle size

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}

In this example, only the file name has changed, but at the same time we save 74kb by executing this part of the code on the server.

Full Backend Access

Since the components are executed on the server, you can get full access to the server environment:

// Note.server.js - Server Component
import fs from 'react-fs';

function Note({id}) {
  const note = JSON.parse(fs.readFile(`${id}.json`));
  return <NoteWithMarkdown note={note} />;
}
// Note.server.js - Server Component
import db from 'db.server';

function Note({id}) {
  const note = db.notes.get(id);
  return <NoteWithMarkdown note={note} />;
}

Only for accessing the server environment, wrappers are required for the react, at the moment the react team has developed 3 wrappers:

  • react-fs – Wrapper for working with files
  • react-fetch – Wrapper for networking
  • react-pg – Wrapper for working with PostgresSql

I think in the near future, we will have an API with which it will be possible to create our own wrappers for working with the server component.

Automatic Code Splitting

If you are working with react, then you are already familiar with the concept of Code Splitting. This is a process where components are loaded as needed, in order to reduce the size of our bundle we use dynamic modules and React.lazy:

// PhotoRenderer.js
// NOTE: *before* Server Components

import React from 'react';

// one of these will start loading *when rendered on the client*:
const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js'));
const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.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} />;
  }
}

When using Server Components, we get this feature by default:

// PhotoRenderer.server.js - 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} />;
  }
}

Since this component is executed on the server, the required component will be sent to the client, which allows writing more familiar and intuitive code.

Lack of client-server waterfalls

A waterfall is when, after rendering, we need to request data from the server, after receiving it, perhaps we need to get some more data, and thus we need to execute many requests to the server, and the user must wait for these requests to be completed.

This approach degrades the user experience when using the application. There are different solutions, most often they relate to the API. For example graphql / JSON API and others.

Most often, we write components like this:

// Note.js
// NOTE: *before* Server Components

function Note(props) {
  const [note, setNote] = useState(null);
  useEffect(() => {
    // NOTE: loads *after* rendering, triggering waterfalls in children
    fetchNote(props.id).then(noteData => {
      setNote(noteData);
    });
  }, [props.id]);
  if (note == null) {
    return "Loading";
  } else {
    return (/* render note here... */);
  }
}

Here we loaded the Note component itself, after that we rendered and only after that we requested data from the server. Now with server components we can do it not sequentially, but simultaneously:

// Note.server.js - Server Component

function Note(props) {
  // NOTE: loads *during* render, w low-latency data access on the server
  const note = db.notes.get(props.id);
  if (note == null) {
    // handle missing note
  }
  return (/* render note here... */);
}

Thus, when a component is executed on the server, it can interact with the necessary api and transfer data to the component in one iteration over the network for the user, after which the result of the component’s execution is streamed to the user

Outcome

In my opinion, server components have a place to be, combining Suspence, Concurent Mode and Server Components can be flexible for developers and user-friendly to implement UI.

Don’t forget this RFC and approach, implementations and APIs may change prior to official release.