DEV Community

Cover image for Implementing Server Side Rendering using React and Express
Anshuman Chhapolia
Anshuman Chhapolia

Posted on • Edited on

Implementing Server Side Rendering using React and Express

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 


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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"
    ]
  ]
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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",
  }
    ....
    ....
}


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

Now if we run the server. We get our react components rendered. But the page is not reactive.

Without Hydrate

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"));



Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

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.🎉🎉🎊

Feel free to comment if you have any doubts regarding this.
final

Top comments (9)

Collapse
 
anirudhrowjee profile image
Anirudh Rowjee

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 be npm install @babel/cli. I hope I'm able to help some other complete noob (like me) who was confused about what was going on.

Collapse
 
achhapolia10 profile image
Anshuman Chhapolia

Thank you Anirudh for pointing that out.

Collapse
 
96erardo profile image
Gerardo García Lopez • Edited

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 ?

Collapse
 
achhapolia10 profile image
Anshuman Chhapolia

Can I have a look at the webpack config?

Collapse
 
sam13591980 profile image
sam13591980

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

Collapse
 
anastasiasidorenko profile image
Anastasia

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?

Collapse
 
achhapolia10 profile image
Anshuman Chhapolia

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.

Collapse
 
lailys profile image
Laily Sarvarian

may I know where is the part that compiles ejs to the public folder?

Collapse
 
achhapolia10 profile image
Anshuman Chhapolia

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.