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" | |
} | |
} | |
] | |
} | |
} | |
]; |
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}!`)) |
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 \"./ \"" | |
}, |
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") | |
); |
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

your output file will be created as follows:

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> | |
); | |
} | |
} |
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")); |
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}!`)); |
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!

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> | |
); | |
} | |
} |
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; |
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}!`)); |
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") | |
); |
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> | |
); | |
} | |
} |
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> | |
); | |
} | |
} |
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.
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.
157