React SSR : Get started with server side rendering react app ASAP

Introduction

Hello everyone, This React SSR article is for those developers who love control. Every project they work on, they want to own the code, want to know the project in and out, how the data flows from one component to the other. So if you are looking for a quick set up for react and don’t care about what goes on under the hood, it’s better you start the project with create-react-app or nextjs. Let’s get started, shall we!

The plan

The following things are to be covered for our REACT SSR setup. we will cover each in detail one by one.

  • installing dependencies
  • webpack config
  • client-side setup
  • server-side setup
  • routing setup

Part 1: basic setup

Creating Project

create the project with the following command

npm init

fill all the requested details and press enter. you will see that a new file is created in your project directory.

Installing dependencies

Install the following NPM packages

  • react
  • react-dom
  • react-router-dom
  • express
  • webpack (dev-dep)
  • webpack-CLI (dev-dep)
  • @babel/core (dev-dep)
  • @babel/preset-env (dev-dep)
  • @babel/preset-react (dev-dep)
  • babel-loader (dev-dep)
  • npm-run-all (dev-dep)
  • nodemon (dev-dep, better to be installed globally)

All the packages with “dev-dep” are development dependency packages and won’t be compiled to production. Install these packages with the following commands:

yarn add react react-dom express react-router-dom

yarn add -D webpack webpack-cli @babel/core @babel/preset-env @babel/preset-react babel-loader nodemon npm-run-all

Once these packages are installed we can now start configuring Webpack so that we can use ES 6+ standards for coding and bundle source files for an optimized production file.

Create the webpack.config.js file and add the following code:

const path = require("path");
module.exports = [
{
entry: {
client: "./src/client/index.js"
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js"
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
},
{
target: "node",
entry: {
server: "./src/server.js"
},
node: {
dns: "mock",
fs: "empty",
path: true,
url: false,
net: "empty"
},
output: {
path: path.resolve(__dirname),
filename: "[name].js"
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
}
];
view raw webpack.config.js hosted with ❤ by GitHub

In the above config, I have added config for compiling both react files and server files. Do note that in the server config, we need to add target as “node” and set the config for DNS, fs, path, URL and net. Without setting those, Webpack will throw an error while compiling. To learn more about configuring Webpack, go here.

Defining server.js file

Now that our webpack config is ready, let’s get on with defining our server file. Add the following code in your server.js file:

import express from 'express';
const app = express()
const port = 3000
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
view raw src - server.js hosted with ❤ by GitHub

One server file is defined we can update our package.json file with the npm start scripts as follows:

"scripts": {
"start": "npm-run-all --parallel watch:build watch:server",
"watch:build": "webpack --watch",
"watch:server": "nodemon \"./server.js\" --watch \"./ \""
},
view raw package.json hosted with ❤ by GitHub

Defining client index file

As per our webpack config file we also need a client index file in which we will code ours react app. for now just add the following basic react code to see if everything works.

import React from "react";
import { hydrate } from "react-dom";
hydrate(
<div>Hello World</div>,
document.getElementById("app")
);
view raw client - index.js hosted with ❤ by GitHub

Output

Once this is done, just open a terminal in your project directory and run the command “npm start”, you should be able to see the following result at localhost:3000

Hello world

your output file will be created as follows:

React ssr file strucrure after webpack compile
output file directory

Part 2: Render react app from serverside

We already have a react app defined on our client-side. We will update it with an app component which will be used in common by both client and server side while rendering. Create a new file called app.js and add the following code.

import React, { Component, Fragment } from "react";
export default class App extends Component {
render() {
return (
<Fragment>
<div>Hello World</div>
</Fragment>
);
}
}
view raw app.js hosted with ❤ by GitHub

Now that the app component is ready we will update our index.js file accordingly. Now you will knwo why I used hydrate method instead of render. It is to keyy the rendering in sync with the serverside . Once the app is loaded from the serverside, the client side can get in sync with the hydrate method and can take over without dependency on serverside to access the other pages. In short, the app will work without reload when . you click on links for other pages.

import React from "react";
import { hydrate } from "react-dom";
import App from "./app";
hydrate(<App />,document.getElementById("app"));
view raw index.js hosted with ❤ by GitHub

Rendering app from serverside

The same app component from the client-side will be called and rendered when browser reloads and the app is loaded from the server-side. To make it happen, just add the following code into your server.js file

import express from "express";
import React from "react";
import { renderToString } from "react-dom/server";
import App from "./client/app";
const app = express();
const port = 3000;
const HTML = (req, context) => {
const body = renderToString(<App />);
return `<html>
<head>
<title>React basic SSR </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body style="margin:0;">
<div id="app">
${body}
</div>
<script src="client.js"></script>
</body>
</html>`;
};
const context = {};
app.use(express.static("dist"));
app.get("/", (req, res) => {
return res.send(HTML(req, context));
});
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
view raw server.js hosted with ❤ by GitHub

renderToString is a special method provided by react to convert a react component or HTML to string. Once our app component is converted to a string we can directly send it as the response for the browser’s request for the react app.

If everything works as expected, you should be able to see the hello world output as before, the difference is that your React app is now server-side rendered. Rejoice!! you now know how to set up your basic React SSR from scratch!

Rejoice from split

Part 3: Let’s setup Routing

Every React app with multiple pages need routing defined to access them. We will create a separate file named routes.js to define our routes. In this example, I am going to keep it basic and maintain routing between 2 pages.

Creating basic route page components

Before defining the routes we need to create the page components for which we are defining routes. We need 3 page components:

  • Home
  • About
  • Not Found (for any other url which is not defined)

For your understanding, I will show the page component definition for home page, similarly you can define the other 2 components.

import React, { Component, Fragment } from "react";
export default class Home extends Component {
render() {
return (
<Fragment>
<p>this is Home</p>
</Fragment>
);
}
}
view raw home.js hosted with ❤ by GitHub

In the above code, if you are not familiar with what Fragment is, check out this link, here. Once components are defined, create file named routes.js in your src directory and add the following code in it.

import Home from "./client/pages/home/home";
import About from "./client/pages/about/about";
import NotFound from "./client/pages/not-found/not-found";
const Routes = [
{
url: "/",
exact: true,
component: Home
},
{
url: "/about",
exact: false,
component: About
},
{
url: "*",
exact: true,
component: NotFound
}
];
export const MenuLinks = [
{
url: "/",
menuText: "Home"
},
{
url: "/about",
menuText: "About"
}
];
export default Routes;
view raw routes.js hosted with ❤ by GitHub

As you can see, I have also defined an array for menu links so that I can iterate through it and render the links dynamically in the app header.

Updating routes on server-side

In the server.js file, we will use the Routes defined earlier in our server app to define it’s routing. update your server.js as follows:

import express from "express";
import React from "react";
import { renderToString } from "react-dom/server";
import { StaticRouter as Router } from "react-router-dom";
import path from "path";
import App from "./client/app";
const app = express();
const port = 3000;
import Routes from "./routes";
const HTML = (req, context) => {
const body = renderToString(
<Router location={req.url} context={context}>
<App />
</Router>
);
return `<html>
<head>
<title>React Basic SSR</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body style="margin:0;">
<div id="app">
${body}
</div>
<script src="client.js"></script>
</body>
</html>`;
};
const context = {};
app.use(express.static("dist"));
app.get("*", (req, res) => {
return res.send(HTML({ url: "/404" }, context));
});
Routes.forEach(route => {
app.get(route.url, (req, res) => {
return res.send(HTML(req, context));
});
});
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
view raw server.js hosted with ❤ by GitHub

In the above code you can see that i have defined a constant HTML which contains the basic html template string in which we will inject the app which is wrapped in static router provided by react-router-dom as follows:

import { StaticRouter as Router } from "react-router-dom";
.
.
.
<Router location={req.url} context={context}>
      <App />
</Router>

Additionally you will also have to wrap the app component on the client side the same way but instead of static router, you will have to use browser router. Update the code for your client side index.js as follows:

import React from "react";
import { hydrate } from "react-dom";
import App from "./app";
import { BrowserRouter as Router } from "react-router-dom";
hydrate(
<Router>
<App />
</Router>,
document.getElementById("app")
);
view raw index.js hosted with ❤ by GitHub

Updating routes on client side

In our app component js file, we have to declare all the routes defined in the routes.js file so that every page defined is available on the client side. Add the following code in your app.js file

import React, { Component, Fragment } from "react";
import { Switch, Route } from "react-router-dom";
import Routes from "../routes";
import Header from "./components/header/header";
export default class App extends Component {
render() {
return (
<Fragment>
<Header />
<Switch>
{Routes.map((c, index) => (
<Route key={index} path={c.url} exact component={c.component} />
))}
</Switch>
</Fragment>
);
}
}
view raw app.js hosted with ❤ by GitHub

In the above code I have added a switch tag which conditionally renders the page component when a react link is clicked. Also you can see that I have imported a header component which contains the menu links for both home and about pages. When you click on one of the links rendered, the component for the clicked page renders accordingly. So, create a components folder inside the src directory and add a header folder inside which add the following code:

import React, { Component,Fragment } from "react";
import { MenuLinks } from "../../../routes";
export default class Header extends Component {
render() {
return (
<Fragment>
{MenuLinks.map((menu, index) => (
<Link to={menu.url}>{menu.menuText}</Link>
))}
</Fragment>
);
}
}
view raw header.js hosted with ❤ by GitHub

Output

If everything is set as shown above, you should see the output as follows when you run the command “npm start”. Once you get the following output you can start adding pages in the pages folder as you need. All you need to make sure is that you define the routes in the routes file accordingly so that it will be mapped to both client and server side. for any other url (apart from the one’s defined in routes.js file) accessed by the user, it will render the not-found page.

react ssr output

Conclusion

Now that your basic react SSR app is ready, you can scale it to your needs. There are many more additions we can do like configuring browser reload, configuring redux, adding support for styled-components from the server-side, etc. I will be adding articles for all those soon. If you have any doubts/issues with the concepts explained in the article, feel free to add a comment below and I will clear it up ASAP. For further reference, Please check the GitHub repository which contains the entire project of basic React SSR, here. Happy Coding!!

Before you go…

I recently added a few improvements to my original SSR project. If you have followed the steps mentioned above you should now clearly know how to create an SSR app, but for your next project, if you want to just, bootstrap and avoid manually creating the app, you can go through the Quick setup guide for creating React SSR with HMR(Hot module reload feature) here.

154
154