In a recent talk, the React team announced a new feature called React Server Components (RSC). What is that exactly, and how can we take advantage of it to write better apps?
If you are familiar with React, you know that it is a client-side library that provides developers with a set of abstractions on top of JavaScript that quickly and efficiently write the user interface to a web application. A client-side library means rendering the view in the DOM is done on the client’s browser using JavaScript. The server, in this case, is only responsible for delivering the bundles of your application containing HTML, CSS, and JavaScript and doesn’t perform any rendering.
The server sends a response back in HTML containing an empty body and script tags that reference JavaScript bundles in the head. That means JavaScript files must first be downloaded to the user’s browser before the rest of the page starts loading. This has two significant drawbacks:
- Decreased performance as the initial load time increases
- Lousy SEO, as many web crawlers can’t parse and read content from JavaScript files
After loading the initial JavaScript file, the content can be loaded asynchronously. The critical content is loaded first and then the non-critical content later, but this still introduces performance problems. To solve these performance issues, developers resort to reducing the bundle size of their React applications using minification, code splitting, dead code elimination, and so on. However, often this is not enough.
In this article, we’ll give you a deep dive into React Server Components, an experimental feature that can help you overcome these performance obstacles.
React Server Components
According to research by Google, 53 percent of mobile website visitors will leave if a webpage doesn’t load within three seconds. You can see why that’s a problem for apps built using React or other modern front-end frameworks such as Angular or Vue.
However, an efficient solution exists. We can render React components in HTML on the server thanks to server-side rendering (SSR). The concept of server-side rendering is not new. It has emerged with the modern client-side JavaScript-heavy libraries and frameworks that do the bulk of their rendering on the client.
SSR rendering works by rendering a part of the application on the server and sending it as HTML. The browser starts immediately painting the UI without waiting for JavaScript algorithms to render the views to the DOM before showing users some initial content. This results in improved user experience by increasing user-perceived performance.
React is component-based. You must write your UI as a set of components with parent-child relationships. These components can be either functions such as React hooks or classes that extend the built-in Component class.
React Server Components are the usual React components, but the server renders them instead of the client. This technique enables developers to fetch already-rendered components from the server. Since we already have SSR techniques used by developers, with many great and easy-to-use tools — like Nest.js, Gatsby or even Express.js — what’s unique about React Server Components?
Note: Next.js is a popular framework that makes it easy to create server-side rendered React apps without the hassle of configuring that by yourself.
At first sight, RSC seems like regular server-side rendering, but it opens the doors to writing apps with extra benefits such as:
- Zero effect on the final bundle size
- Direct access to the backend resources
- Use of React IO libraries such as react-fs (filesystem), react-pg (Postgres), react-fetch (Fetch API)
- Granular control over the components that the client must download
Zero effect on the final bundle size means that RSC allows your React application to use third-party utility libraries without affecting the client’s bundle size. How is that possible?
Let’s use this example of a server component:
import marked from 'marked';
import sanitizeHtml from 'sanitize-html';
// [...]
export default function TextWithMarkdown({text}) {
return (
<div
className="text-with-markdown"
dangerouslySetInnerHTML=\{{
__html: sanitizeHtml(marked(text), {
allowedTags,
allowedAttributes,
}),
\}}
/>
);
}
This component imports two external libraries, marked and sanitize-html. If you use this as a client component, the final bundle also contains these two libraries. They are required by the sanitizeHtml(marked(text), {}) call to sanitize and convert the passed text to Markdown. Thanks to RSC, the server executes the code. The server returns only the final converted text. The libraries are not needed at runtime and are not included!
Now, what about accessing the server resources directly and React IO libraries? Server resources can range from files to fully-fledged databases, which are essential for building full-stack data-driven apps.
RSC is in the research phase, but this suggests that we can use React to build full-stack apps that work in the same way traditional apps work. You can use server components to interact with the databases and the file system on the server and return the results to the client. That means you can choose to avoid using REST or GraphQL APIs to exchange data between the client and server!
When building business apps, we typically must use a database. With React Server Components, we can access this database from the part of our React app running on the server and return results to the client alongside the rendered component itself instead of only the JSON data we'd send to a fully client-side React application.
Thanks to RSC, we can build web applications in old app architecture while still having modern UIs. For beginners who don’t want to learn REST or GraphQL but still want to build full apps not just with one language (JavaScript) but also with one library, React makes it more straightforward than the old days when you had to use PHP with HTML and JavaScript to build a full-stack app.
The React team collaborates with other teams to implement this feature into meta-frameworks like Next.js and Gatbsy using a webpack plugin. However, this doesn’t mean that you can’t use the feature without these tools if you like.
In SSR, we render the components to HTML and send the results to the client. React Server Components are rendered to a JSON format and streamed to the client:
{
"id": "./src/App.client.js",
"chunks": ["main"],
"name": ""
}
React Server Components Demonstration
Now that we have explored what React Server Components are and their benefits let’s create a step-by-step demonstration. Please note that this is still an experimental technology, so the APIs presented here may change in the future.
Since RSC is still an experimental feature, we’ll manually create our project instead of using the create-react-app. We’ll use this project’s template forked from the official demo.
Head over to a new command-line interface and start by running the following commands:
git clone https://github.com/techiediaries/rsc-project-template rsc-demo
cd rsc-demo
Now, you’ll have a package.json file and a webpack.config.js file in your folder.
You will notice that we included several dependencies with an experimental version in the package.json file. We included the principal dependencies, which are react, react-dom, and the react-server-dom-webpack. We used experimental versions that provide support for React Server Components.
In our demonstration, we use Webpack to build apps and Babel to transpile React code to plain JavaScript. We run our server with Express.js and use concurrently to run multiple commands concurrently. The tool nodemon helps develop node.js-based applications by automatically restarting the node application when file changes in the directory are detected.
As a development dependency, we included cross-env, which makes it easy to have a single command for setting and using environment variables properly for the target platform.
Finally, we have some npm scripts to start the development server and build the production bundles using the concurrently, cross-env and nodemon packages and Webpack:
"scripts": {
"start": "concurrently \"npm run server:dev\" \"npm run bundler:dev\"",
"start:prod": "concurrently \"npm run server:prod\" \"npm run bundler:prod\"",
"server:dev": "cross-env NODE_ENV=development nodemon -- --conditions=react-server server",
"server:prod": "cross-env NODE_ENV=production nodemon -- --conditions=react-server server",
"bundler:dev": "cross-env NODE_ENV=development nodemon -- scripts/build.js",
"bundler:prod": "cross-env NODE_ENV=production nodemon -- scripts/build.js"
},
Now, run the following command to install these dependencies:
npm install.
Next, create a public/index.html file and add the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<title>React Server Components Demo</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
We added a <\div> with the root ID to mark where we can render our React components tree.
Next, create a src/index.client.js file and add the following code:
import { unstable_createRoot } from 'react-dom';
import App from './App.client';
const root = unstable_createRoot(document.getElementById('root'));
root.render(<App />);
First, import the unstable_createRoot method for enabling concurrent mode for the whole < App /> tree. Concurrent Mode APIs such as createRoot only exist in the experimental versions of React.
Next, call the render method of the root object returned from the unstable_createRoot method to render the App components and its children in the DOM element with the root ID retrieved using the getElementById method.
The App component is imported from an App.client.js file which we create later.
Next, create a src/Cache.client.js file and add the following code:
import {unstable_getCacheForType} from 'react';
import {createFromFetch} from 'react-server-dom-webpack';
function createResponseCache() {
return new Map();
}
export function useServerResponse(props) {
const key = JSON.stringify(props);
const cache = unstable_getCacheForType(createResponseCache);
let response = cache.get(key);
if (response) {
return response;
}
response = createFromFetch(
fetch('/react?props=' + encodeURIComponent(key))
);
cache.set(key, response);
return response;
}
First, import the unstable_getCacheForType and the createFromFetch methods. Next, create a response cache using the JavaScript Map data structure. You use this to store collections of keyed data items. Fetch the server component using the Fetch API and pass the results to the createFromFetch method to create a convenient response object. Pass the response object to the cache using the Map.set method.
Next, create a src/App.server.js file and add the following code:
import marked from 'marked';
export default function App(props) {
return (
<div>
<h3>
Markdown content rendered on the server
</h3>
<div
dangerouslySetInnerHTML=\{{
__html: marked(props.mdText)
\}}>
</div>
</div>
)
}
Here create a React component that accepts an mdText prop and convert its Markdown content to HTML using the marked library, then set the results as the inner HTML of a <\div>.
Since this component’s file ends with the server.js name, this component is a React Server Component rendered on the server.
Next, create a src/App.client.js file and add the following code:
import {useState, useRef, Suspense} from 'react';
import {useServerResponse} from './Cache.client';
const title = 'React Server Components Demo';
const RenderedContent = (props) => {
const response = useServerResponse(props)
return response.readRoot()
}
export default function App() {
const [content, setContent] = useState('');
const contentRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
setContent(contentRef.current.value);
};
return (
<Suspense fallback={<div>Loading...</div>}>
<div>
<h2>{title}</h2>
<form onSubmit={ handleSubmit }>
<textarea ref = { contentRef }
name="content"
>
</textarea>
<br />
<input
type="submit" value="Convert.."
/>
</form>
</div>
<RenderedContent mdText={content}></RenderedContent>
</Suspense>
);
}
Create two components, RenderedContent to accept a prop for Markdown text and call the useServerResponse to fetch the response from the app server component that returns the rendered markdown text.
Create a new reference by calling React.useRef hook and associate it with the form’s textarea element where we submit the markdown text to send to the server component as a prop.
We used the Suspense component to asynchronously load the component and specify a loading UI that displays the loading text while the user is waiting. This allows us to build a smoother and more responsive UI.
Finally, create a server/index.server.js file and add the following code:
'use strict';
const register = require('react-server-dom-webpack/node-register');
register();
const babelRegister = require('@babel/register');
babelRegister({
ignore: [/[\\\/](build|server|node_modules)[\\\/]/],
presets: [['react-app', {runtime: 'automatic'}]],
plugins: ['@babel/transform-modules-commonjs'],
});
const express = require('express');
const compress = require('compression');
const {readFileSync} = require('fs');
const {pipeToNodeWritable} = require('react-server-dom-webpack/writer');
const path = require('path');
const React = require('react');
const ReactApp = require('../src/App.server').default;
const PORT = 4000;
const app = express();
app.use(compress());
app.use(express.json());
app.use(express.static('build'));
app.use(express.static('public'));
app.listen(PORT, () => {
console.log(`RSC Demo listening at http://localhost:${PORT}`);
});
app.get(
'/',
async (req, res) => {
const html = readFileSync(
path.resolve(__dirname, '../build/index.html'),
'utf8'
);
res.send(html);
}
);
app.get('/react', function(req, res) {
const props = JSON.parse(req.query.props);
res.set('X-Props', JSON.stringify(props));
const manifest = readFileSync(
path.resolve(__dirname, '../build/react-client-manifest.json'),
'utf8'
);
const moduleMap = JSON.parse(manifest);
return pipeToNodeWritable(React.createElement(ReactApp, props), res, moduleMap);
});
Here, we set up a simple Express.js server, and we expose a /react endpoint that our client code calls to put the rendered component on the server. In the endpoint handler, we read the passed props from the request object, and we call the pipeToNodeWritable method to render the server component and stream it to the response object. This method accepts two arguments, the React component with its props and a module map generated by Webpack using the react-server-dom-webpack/plugin plugin.
Now, run the following command in the root of your project’s folder:
npm start.
The app will be listening on http://localhost:4000/. This is a screen capture of what you see:
Note that we have three types of extensions for the component files:
- .server.js, which indicates a Server Components
- .client.js, which indicates React Client Components
- The regular .js extension is for shared components, which run on the server or the client, depending on who’s importing them.
This article introduced you to React Server Components, a new experimental feature that allows you to render components on the server. This feature provides extra benefits compared to standard server-side rendering techniques, such as zero effect on the final bundle size, direct access to server resources, use of React IO libraries, and granular control over clients’ components.
Access the full code for our sample project, or experiment with RSC yourself. For powerful React tools and components, check out GrapeCity's JavaScript solutions.