Server Side Rendering let us render a basic skeleton of our page server-side and send it to the user. This provides us with benefits like Search engine optimisation and faster initial page load. Let's start from scratch. You can clone this whole project from here.
Prerequisites
- Nodejs
- npm
- Your prefered editor
Basic layout of the project directory
|-- project
|-- .babelrc
|-- package-lock.json
|-- package.json
|-- webpack.config.js
|-- src
|-- server.js
|-- components
| |-- entrypoints
| | |-- index.jsx
| |-- pages
| |-- index.jsx
|-- routes
| |-- index.js
|-- views
|-- pages
| |-- index.ejs
|-- partials
Setting up babel and Express.js
Following command initializes a node package in the current directory.
$ npm init
Next, we install Express and ejs. We will be using ejs as express view engine to write base HTML for the page.
$ npm install express ejs compression react react-dom
Following commands install various packages required for babel.
$ npm install --save-dev @babel/core @babel-cli @babel/node @babel/plugin-proposal-class-properties @babel/preset-env @babel/polyfill @babel/preset-react nodemon webpack webpack-cli
Details about above packages
- @babel /core: Babel's core runtime
- @babel /node: Babel node is a cli that works the same as the Node.js CLI, with the added benefit of compiling with Babel presets and plugins before running it
- @babel /plugin-proposal-class-properties: plugin required by Babel to support classes
- @babel /preset-env : Babel preset that allows using latest JavaScript.
- @babel /preset-react: Babel preset required for react.
- @babel /polyfill: Needed by react to when using promises
- @babel /cli: Optional. Can be used later to compile to the target the application (server and react files) for node to deploy in production.
Configure the babel
Create a file .babelrc in the project directory. If you have any doubt where a file is to be placed refer to the directory structure. Keeping a good structure file is a very important part of code maintenance and is required in huge projects.
.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
],
[
"@babel/preset-react"
]
],
"plugins": [
[
"@babel/plugin-proposal-class-properties"
]
]
}
Setup Server
Create src directory under the project. Add server.js file to it.
./src/server.js
import express from "express";
import compression from "compression";
import index from "./routes/index";
import path from "path";
// Server var
const app = express();
// View engine setup
app.set("views", path.join(__dirname,"views"));
app.set("view engine", "ejs");
// Middleware
app.use(compression());
console.log(__dirname);
app.use(express.static(__dirname + "/public"));
//Routes
app.use("/", index);
const port = process.env.PORT || 3000;
app.listen(port, function listenHandler() {
console.info(`Running on ${port}`)
});
Create a routes directory user src. Add index.js file to routes directory. Add files which implement different routes to this directory.
./src/routes/index.js
import express from "express";
const router = express.Router();
router.get('/', async (req, res) => {
let reactComp = ""; // So that program doesn't break
res.status(200).render('pages/index', {reactApp: reactComp});
})
;
export default router;
Create a directory views which will contain the templates for ejs. Under views, two directories should be made.
- pages: this folder contains the templates for pages that will be representing the skeleton for different pages.
- partials: this folder contains various partials like headers, footers, etc.
Create a file index.ejs at ./src/views/pages.
./src/views/pages/index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<title>Smoke -h</title>
</head>
<body>
<div id="root"><%- reactApp %></div>
<script src="/index.js" charset="utf-8"></script>
<script src="/vendor.js" charset="utf-8"></script>
</body>
</html>
The above-mentioned index.js and vendor.js files are generated by webpack. We will discuss them later.
Add the following script in package.json file.
{
....
....
"scripts": {
"webpack": "webpack -wd",
"dev": "nodemon --exec babel-node src/server.js",
}
....
....
}
I'll explain the webpack script in some time.
The server is ready to run. Run the following command.
$ npm run dev
Open a browser and go to http://localhost:3000. You will see a blank page for now.
Creating React Page
Create a components directory under src. This(components) directory has 2 more sub-directories pages and entrypoints. These directories are locations for our react components. We can create additional folders and file later here as we add more components.
- pages: This directory stores the final pages that need to be rendered.
- entrypoints: This directory store the files that will hydrate our pages. I'll explain this concept later.
Create file index.jsx under project/src/components/pages/. This file is the React page for index route.
./src/components/pages/index.jsx
import React from "react";
class Index extends React.Component {
constructor() {
super();
this.state = {name: "a", email: ""}
}
onFormSubmit = (event) => {
event.preventDefault();
}
onNameChangeHandler = (event) => {
this.setState({name: event.target.value});
}
onEmailChangeHandler = (event) => {
this.setState({email: event.target.value});
}
render() {
return (
<div>
<h1>Smoke -h</h1>
<form onSubmit={this.onFormSubmit}>
<div>
<label htmlFor={"name-input"} defaultValue={"Name"}>Name: </label>
<input name={"name-input"} onChange={this.onNameChangeHandler} type={"text"}
value={this.state.name}/>
</div>
<br/>
<div>
<label htmlFor={"email-input"} defaultValue={"Email"}>Email: </label>
<input name={"email-input"} onChange={this.onEmailChangeHandler} type={"email"}
placeholder={"email"} value={this.state.email}/>
</div>
<br/>
<div>
<button type={"submit"}>Submit</button>
</div>
</form>
<span><h5>Name: {this.state.name}</h5></span>
<span><h5>Email: {this.state.email}</h5></span>
</div>
)
}
}
export default Index;
Rendering React Page on Server Side
We now use renderToString() to render the react components in our ./src/routes/index.js file.
./src/routes/index.js
import express from "express";
import React from "react";
import {renderToString} from "react-dom/server"
import Index from "../components/pages/index"
const router = express.Router();
router.get('/', async (req, res) => {
const reactComp = renderToString(<Index/>);
res.status(200).render('pages/index', {reactApp: reactComp});
})
;
export default router;
Now if we run the server. We get our react components rendered. But the page is not reactive.
Hydrating the React Pages
ReactDOM.hydrate() is the function that brings ours react page alive. When we call ReactDOM.hydrate() it preserves the server-rendered markup and attach event handlers to them and provide an entry point into the react application.
To create this entrypoint we create index.jsx under ./src/components/entrypoints/index.jsx.
./src/components/entrypoints/index.jsx
import React from "react";
import {hydrate} from "react-dom";
import Index from '../pages/index'
hydrate(<Index/>, document.getElementById("root"));
But doing so is not enough. This part of the code should run on the client-side. That is why use webpack to bundle this file with libraries to send it to frontend side.
Webpack
Webpack is a bundler. A module bundler is a tool that takes pieces of JavaScript and their dependencies and bundles them into a single file, usually for use in the browser.
To use webpack we need to create a webpack.config.js file under our project directory.
webpack.config.js
const path = require("path");
const config = {
entry: {
vendor: ["@babel/polyfill", "react"], // Third party libraries
index: ["./src/components/entrypoints/index.jsx"]
/// Every pages entry point should be mentioned here
},
output: {
path: path.resolve(__dirname, "src", "public"), //destination for bundled output is under ./src/public
filename: "[name].js" // names of the bundled file will be name of the entry files (mentioned above)
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: {
loader: "babel-loader", // asks bundler to use babel loader to transpile es2015 code
options: {
presets: ["@babel/preset-env", "@babel/preset-react"]
}
},
exclude: [/node_modules/, /public/]
}
]
},
resolve: {
extensions: [".js", ".jsx", ".json", ".wasm", ".mjs", "*"]
} // If multiple files share the same name but have different extensions, webpack will resolve the one with the extension listed first in the array and skip the rest.
};
module.exports = config;
Now run
$ npm run webpack
On another shell run
$ npm run dev
And now when we visit http://localhost:3000 our react app has become dynamic.🎉🎉🎊
Top comments (9)
I haven't finished the article yet, but I'd just like to point out that the steps read
npm install @babel-cli
, whereas that should benpm install @babel/cli
. I hope I'm able to help some other complete noob (like me) who was confused about what was going on.Thank you Anirudh for pointing that out.
I am using a setup very similar to this, and is working good so far (i am barely starting), the thing is that ihave a problem bundling assets, the thing is that i setup a loader in the babel and webpack configuration and when both compilers run, the image names are different and repeated in the public directory, do you know what is the best approach to handle this situation ?
Can I have a look at the webpack config?
Thank you for your tutorial.
I am still not sure I got what exactly you do. But I am looking for a way to render the react in client side but still I could load ejs files in the server and serve it as an entry point to React.
If it is possible to do like it, I would appreciate to let me know how!
Thanks
Hi, I started to make some site using this tutorial and I've encountered problem that when I load page I see what is in localhost and second later I see what is cached in webpack storage. I've tried many solutions and can't solve it. Could I ask for some help?
Hi Anastasia, thank you for using my tutorial. I would recommend using github.com/achhapolia10/forum as your base project. I've made a little improvements to the current structure. I'm going to make a new post on the latest structure in a day or two.
I guess you are having an issue with understanding entrypoints. To add a new page in current project you need to add an entrypoint file for each new page you want to render, an ejs file which will act as a base template for that page (a little changes needed to be made to a copy of index.ejs file), and an entry to the webpack.config.js.
TLDR; It's a lot of effort to add a new page.
My next post is on how to automate many of these tasks and also tells how to include images and CSS efficiently reducing bundle sizes. The above repository is the companion repository for the new post.
may I know where is the part that compiles ejs to the public folder?
ejs is not compiled to public folder. EJS is compiled and rendered at runtime. We bundle the React hydrate so that we can serve that with the rendered html. You can find it in webpack.config.js. You can refer to this repository. It contains frontend for a forum with the same concept. This will also give an idea how to serve CSS and other assets.