How to Implement SSR(Server Side Rendering) in React 18

Jay Sheth
Simform Engineering
5 min readSep 15, 2023

Learn how to implement the “renderToPipeableStream” server API to render a React tree as HTML to a Node.js stream.

Server-Side Rendering Flow

React 18, the latest version of the popular JavaScript library for building interactive user interfaces comes with many new features and enhancements. Its improved proficiency at Server Side Rendering (SSR) is a noteworthy feature.

In this article, we’ll explore React’s SSR feature with helpful code samples and examples. But first, let’s differentiate between client-side and server-side rendering.

Client-side rendering (CSR) is the process of rendering web pages on the client side (i.e., in the user’s web browser). The server merely provides the raw data or content, which the client-side JavaScript utilizes to construct the final rendered page dynamically.

Server-side rendering (SSR) refers to the process of rendering web pages on the server before sending them to the client’s web browser. Rather than transmitting raw data and depending on the client-side, this approach involves the server generating the final HTML markup for a web page and sending it to the client.

Implement “renderToPipeableStream” Server API

Step 1: Create a new React application using the create-react-app command line tool. Open your favorite terminal and write the below command.

npx create-react-app server-api-demo-app

Step 2: Switch to your newly created React app.

cd server-api-demo-app

Step 3: Now, add react-router-dom in the project to handle routing.

npm install react-router-dom

Step 4: Let’s add some pages to your application. In the app.js, you can add the sample routes as added below:
(i) Home
(ii) About

const App = () => (
<div>
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/about" element={<About />}></Route>
</Routes>
</div>
);

Step 5: Add some content to both pages. For reference, click here.

Step 6: Create a new folder server at the root level, and in there — new files index.jsand server.js. Copy and paste the below code in that file.

// server/index.js
require("ignore-styles");

require("@babel/register")({
ignore: [/(node_modules)/],
presets: ["@babel/preset-env", "@babel/preset-react"],
});

require("./server");

This code fragment sets up Babel for code translation, filters out certain files like those in “node_modules”, and launches the server by importing the “server” module. This setting is commonly used in React server-side rendering to allow the server to process and serve React components to clients.

// server/server.js
import express from "express";
import React from "react";
import ReactDOMServer from "react-dom/server";
import { StaticRouter } from "react-router-dom/server";
import App from "../src/App";

const app = express();

app.get("/*", (req, res) => {
const entryPoint = ["/main.js"];

const { pipe, abort: _abort } = ReactDOMServer.renderToPipeableStream(
<StaticRouter location={req.url}>
<App />
</StaticRouter>,
{
bootstrapScripts: entryPoint,
onShellReady() {
res.statusCode = 200;
res.setHeader("Content-type", "text/html");
pipe(res);
},
onShellError() {
res.statusCode = 500;
res.send("<!doctype html><p>Loading...</p>");
},
}
);
});

app.listen(3002, () => {
console.log("App is running on http://localhost:3002");
});

The code defines a route handler for all routes using app.get("/*", ...). This means that this route handler will handle any incoming request to the server. Inside the route handle:

  • The entryPoint array is defined with the value main.js. This points to the JavaScript file used to bootstrap the client-side code.
  • ReactDOMServer.renderToPipeableStream() accepts two arguments: a React Node for HTML rendering and an optional options object with streaming options. It returns an object with two methods: pipe and abort. The pipe method outputs HTML to the specified Node.js stream. We use pipe in onShellReady to enable streaming. For static generation and crawlers, onAllReady can also be used.
  • The onShellReady() is triggered when the rendering process is complete, and the HTML is ready for client transmission. It sets the response status code to 200, defines the content type header as text/html, and pipes the rendered HTML to the response using the pipe method.
  • The onShellError() callback is triggered when an error occurs during rendering. It sets the response status code to 500 and sends a basic error message enclosed in an HTML <p> tag.

7. On the client side, we need to update ReactDOM.createRoot with ReeactDOM.hydrareRoot in index.js file to make the server-generated HTML interactive.

// index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

ReactDOM.hydrateRoot(
document,
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

8. Add the below script in package.json file to run the code on the server side.

"ssr": "npm run build && node server/index.js"  

This command will build your project, run the code on the server side, and generate the output on localhost:3002

9. Now, run npm run ssr command to see the output.

SSR Output in the DevTools Network tab

You can find the complete source code on the GitHub repo 📌.

Here, we have demonstrated one “renderToPipeableStream” API only. React also provides other APIs like “renderToNodeStream”, “renderToReadableStream”, “renderToStaticMarkup”, “renderToStaticNodeStream”, and “renderToStream” for server-side rendering based on our requirements.

For detailed information about these APIs, you may refer to the official documentation.

Conclusion

With these new Server APIs, we can render React components to server-rendered HTML, either as a Node.js stream or a Web stream.

In many cases, frameworks such as Next.js, Remix, and Gatsby may handle this process automatically. These APIs are only used to build the server-rendered HTML at the top level of your app, which will improve initial load time, SEO, user experience, and reduced vulnerability to cross-site scripting (XSS) assaults.

However, while SSR offers advantages, it also comes with drawbacks such as complex implementation, increased server load that can consume substantial amounts of processing and memory, and may not be suitable for real-time applications like chat apps and multiplayer games.

So, consider your requirements and ensure SSR implementation aligns with them.

For more updates on the latest tools and technologies, follow the Simform Engineering blog.

Follow Us: Twitter | LinkedIn

--

--