Adding style to Server-Side rendering and automating the build process
Tags: react, express, webdev, javascript
Published: 2020-05-15 16:20:59

In the previous post in series, I demonstrated how we can easily set up a project for Server-Side Rendering from scratch. I appreciate @silverspear28 and @coderbhaai for building their application using the previously proposed structure. Their feedback was very helpful in improving this project.

## Prerequisites

* Node.js
* npm
* Your preferred editor

Note: You must have basic knowledge of React and Express before moving forward. Best place to learn React is [React Documentation](https://reactjs.org/docs/getting-started.html). Express is the de facto standard framework for Node.js and you can find many resources online to learn it.

## Things I'll cover in this post

1. Creating a project for SSR from scratch (including writing babel and webpack configs).
2. Adding styles and images to our page

I'll be explaining webpack and babel in detail so people who are not familiar with these can also understand the config files.

## Setting up the project directory.

I've already covered this in the previous post. Please refer it for the details and explanation of the steps performed.

<div class="highlight">
<pre class="highlight plaintext">
<code>
|-- project
     |-- <a href="#babelrcjs">.babelrc.js</a>
     |-- package-lock.json
     |-- <a href="#packagejson">package.json</a>
     |-- <a href="#webpackconfigjs">webpack.config.js</a>
     |-- src
          |-- <a href="#srcserverjs">server.js</a>
          |-- components
          |   |-- entrypoints
          |   |     |-- <a href="#srccomponentsentrypointsindexjsx">index.jsx</a>
          |-- pages
          |   |-- <a href="#srcpagesindexjsx">index.jsx</a>
          |   |-- index.css
          |   |-- img.jpg
          |-- routes
          |   |-- <a href="#srcroutesindexjs">index.js</a>
          |-- views
               |-- pages
               |    |-- <a href="#srcviewspagestemplateejs">template.ejs</a>
               |-- partials
</code>
</pre>
</div>

Major Changes made to file structure:
* <i>pages</i> directory moved out of *components* directory.
* Replaces *.babelrc* with a *.babelrc.js* to create dynamic configuration for babel.

#### package.json
```json
{
  "scripts": {
    "webpack": "webpack -d",
    "dev": "nodemon --exec 'npm run webpack &&  COMPILER_ENV=server babel-node src/server.js'",
    "webpack-production": "NODE_ENV=production webpack -p",
    "babel-production": "NODE_ENV=production COMPILER_ENV=server babel --out-dir dist src",
    "start": "node dist/server.js",
    "build": "npm run webpack-production && npm run babel-production"
  },
  "author": "smoketrees",
  "license": "MIT",
  "nodemonConfig": {
    "ignore": [
      "src/static/**/*"
    ]
  },
  "homepage": "https://github.com/smoke-trees/forum#readme",
  "devDependencies": {
    "@babel/cli": "^7.8.4",
    "@babel/core": "^7.8.4",
    "@babel/node": "^7.8.4",
    "@babel/plugin-proposal-class-properties": "^7.8.3",
    "@babel/polyfill": "^7.8.3",
    "@babel/preset-env": "^7.8.4",
    "@babel/preset-react": "^7.8.3",
    "babel-loader": "^8.0.6",
    "babel-plugin-file-loader": "^2.0.0",
    "babel-plugin-transform-require-ignore": "^0.1.1",
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^3.5.3",
    "file-loader": "^6.0.0",
    "html-webpack-plugin": "^4.3.0",
    "mini-css-extract-plugin": "^0.9.0",
    "nodemon": "^2.0.2",
    "raw-loader": "^4.0.1",
    "webpack": "^4.41.6",
    "webpack-cli": "^3.3.11"
  },
  "dependencies": {
    "body-parser": "^1.19.0",
    "compression": "^1.7.4",
    "ejs": "^3.0.1",
    "express": "^4.17.1",
    "react": "^16.12.0",
    "react-dom": "^16.12.0"
  }
}
```

#### src/server.js

```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, 'static', "views"));
app.set("view engine", "ejs");

// Middleware
app.use(compression());
app.use('/public', express.static(path.join(__dirname, 'static', 'public')));

//Routes
app.use("/", index);

const port = process.env.PORT || 3000;

app.listen(port, function listenHandler() {
    console.info(`Running on ${port}`)
});
```
A basic express server serving at port 3000.

#### .babelrc.js
A .babelrc.js file is not much different than a .babelrc.js and default exports an object which will represent the .babelrc file.

```js
const presets =
  [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ],
    [
      "@babel/preset-react"
    ]
  ]

const plugins = [
  "@babel/plugin-proposal-class-properties",

]

if (process.env.COMPILER_ENV === 'server') {
  plugins.push(
    [
      "file-loader",
      {
        "name": "[hash].[ext]",
        "extensions": ["png", "jpg", "jpeg", "gif", "svg"],
        "publicPath": "/public/img",
        "outputPath": null
      },
      "img-file-loader-plugin"
    ],
    [
      "file-loader",
      {
        "name": "[hash].[ext]",
        "extensions": ["css", "sass", "scss"],
        "publicPath": "/public/css",
        "outputPath": null
      },
      "css-file-loader-plugin"
    ],
  )
}

const addConfigs = { ignore: ["./src/static/"] }

module.exports = { plugins, presets, ...addConfigs }
```

I've added an extra babel plugin to this file called [*babel-plugin-file-loader*](https://www.npmjs.com/package/babel-plugin-file-loader). I would recommend going through its README. It is configured here to convert 
`import styles from 'styles.css'`
to
`const style='/publicPath/[filehash].css'`

It's important to import CSS in an above-mentioned way as simply importing it as ` import 'styles.css' ` will throw an error.

Two instances of the plugins have been configured for this plugin one for the images and another for the css files. Output path for these files is set to null as copying these files to final build will be handled by webpack. Option publicPath is to configure the path where the files will be available through the server.

This babel file is used to transpile the code two times one time when it's bundled by webpack and once when we finally transpile our src folder to run using node. When webpack is using the config file we don't want it to use the *babel-plugin-file-loader* so we've used an environment variable to control when the plugin is used.

#### webpack.config.js
```js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const production = process.env.NODE_ENV === 'production'

const pages = ['index']

const generateEntryPoints = (entry) => {
    return entry.reduce((obj, item) => {
        return {
            ...obj,
            [item]: [path.resolve('src', 'components', 'entrypoints', `${item}.jsx`)]
        }
    }, {})
}

const generateHtml = (entry) => {
    return entry.map((i) => {
        return new HtmlWebpackPlugin({
            chunks: [i],
            filename: `../views/pages/${i}.ejs`,
            template: path.join('src', 'views', 'pages', 'template.ejs')
        })

    })
}

const config = [{
    entry: {
        ...generateEntryPoints(pages)
    },

    output: {
        path: production ? path.resolve(__dirname, 'dist', 'static', 'public') : path.resolve(__dirname, 'src', 'static', 'public'),
        filename: production ? 'js/[chunkhash].js' : 'js/[name].js',
        publicPath: '/public'
    },

    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env', '@babel/preset-react'],
                    }
                },
                exclude: [/node_modules/, /static/]
            }, {
                test: /\.ejs$/,
                loader: 'raw-loader'
            }, {
                test: /\.(css)$/,
                use: [{
                    loader: MiniCssExtractPlugin.loader,
                    options: {
                        publicPath: '/public/css'
                    }

                }, 'css-loader']
            }, {
                test: /\.(jpg|jpeg|png|svg|gif)$/,
                use: [{
                    loader: 'file-loader',
                    options: {
                        name: '[md5:hash:hex].[ext]',
                        publicPath: '/public/img',
                        outputPath: 'img',
                    }
                }]
            }
        ]
    },

    resolve: {
        extensions: ['.js', '.jsx', '.json', '.wasm', '.mjs', '*']
    },

    optimization: {
        splitChunks: {
            automaticNameDelimiter: '.',
            cacheGroups: {
                react: {
                    chunks: 'initial',
                }
            }
        }
    },

    plugins: [
        new CleanWebpackPlugin(),
        // create blog,
        new MiniCssExtractPlugin({
            filename: production ? 'css/[contentHash].css' : 'css/[id].css',
            chunkFilename: production ? 'css/[contentHash].css' : 'css/[id].css'
        }),
        // Ejs pages
        ...generateHtml(pages)
    ]
}]

module.exports = config
```
A lot of plugins have been added to the webpack file and I've written 2 functions to automate the task of creating ejs file.

* [*clean-webpack-plugin*](https://www.npmjs.com/package/clean-webpack-plugin): This plugin cleans the public folder on each run where the output for the webpack is saved.

* [*html-webpack-plugin*](https://www.npmjs.com/package/html-webpack-plugin): HtmlWebpackPlugin plugin is used to create HTML for each page from a template file. The files that are needed to add for the scripts and css will be added automatically for each page depending on its need. I've used raw-loader to load the ejs file as using the default loader of the plugin will cause issues with ejs.

* [*mini-css-extract-plugin*](https://www.npmjs.com/package/mini-css-plugin): MiniCssExtractPlugin is used to extract css from different files and bundle them into different chunks. These chunks will be added automatically thanks to HtmlWebpackPlugin.

* [*file-loader*](https://www.npmjs.com/package/file-loader): *file-loader* is used to copy image files into the public folder.

* [*babel-loader*](https://www.npmjs.com/package/babel-loader): babel-loader is used to transpile the React jsx files before bundling.

* [*css-loader*](https://www.npmjs.com/package/css-loader): css-loader is  used to load css files and resolve imports in the jsx files.

I'll cover the two functions generateHtml and generateEntryPoints later on ["Adding more pages"].

#### src/views/pages/template.ejs
```html
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Smoke -h</title>
</head>

<body>
    <div id="root"><%- reactApp %></div>
</body>

</html>
```
As I mentioned earlier we need not link css files or js scripts ourselves as it will be handled by HtmlWebpackPlugin.

#### src/pages/index.jsx

```jsx
import React from "react";

import global from './global.css'
import index from './index.css'

import img from './img.jpg'

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 className='heading'>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>
                <img src={img} alt='something' />>
            </div >
        )
    }
}

export default Index;
```
#### src/components/entrypoint
```jsx
import React from "react";

import { hydrate } from "react-dom";

import Index from '../../pages/index'


hydrate(<Index />, document.getElementById("root"));
```

#### src/routes/index.js
```jsx
import express from "express";
import React from "react";
import { renderToString } from "react-dom/server"
import Index from "../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;
```

I've also made an update to previous scripts and now you need to run npm run dev to start the development server. It will put the watch on files using nodemon and re-bundle the files and restart the files automatically on change.

Performing the above steps will give the same result as the previous part :stuck_out_tongue_closed_eyes: Until now I've mentioned the changes made as compared to the previous post. Now let's move forward with adding another page to the project and adding a stylesheet and an image.

### Adding a new page to the project.

Adding a new page to our project consist of a few steps.

1) Adding a JSX file for the page in the src/pages directory
```jsx
import React, { Component } from 'react'
import aboutStyle from "./about.css" // please don't import as: import './about.css' as it will cause an error due to babel-plugin-file-loader
import img from "./img.jpg"

export class About extends Component {
    render() {
        return (
            <div className='heading'>
                This is the about page
                <div className='subheading'>
                    This is the sub heading
                    <img src={img}/>
                </div>
            </div>
        )
    }
}

export default About    
```
An image and a css file imported here to demonstrate how we can do that.

2) Add a route handler to express to serve at the new route.
For now, we can do this in the /routes/index.js
```jsx
import express from "express";
import React from "react";
import { renderToString } from "react-dom/server"
import Index from "../pages/index"
import About from "../pages/about"


const router = express.Router();

router.get('/', async (req, res) => {
    const reactComp = renderToString(<Index />);
    res.status(200).render('pages/index', { reactApp: reactComp });
})
router.get('/about', async (req, res) => {   // Add route /about
    const reactComp = renderToString(<About />);
    res.status(200).render('pages/about', { reactApp: reactComp });
});

export default router;
```
3) Create an entry point file inside src/components/entrypoint for the new page.

#### src/components/entrypoint
```jsx
import React from "react";

import { hydrate } from "react-dom";

import Index from '../../pages/about'

hydrate(<Index />, document.getElementById("root"));
```

4) Add an element to the **pages** array in the **webpack.config.js** file.
```js

...
...

const pages = ['index', 'about']

...
...
```

Note: The name of the element added to the **pages** array is the same name as the entrypoint file.

You should add an image as *img.jpg* and a css file *about.css* with some styling in the src/pages directory.

## How to run

Simply run following command on the terminal

```
npm run dev
```

It will put a watch on files and re-bundle the files and restart the server on changes.

## How to build for production

```
npm run build
```

The above command will create production bundles and compile JSX code to commonjs which can be used run using node.js

## Starting production server

```
npm start
```

---
### Ending notes

I've created a companion [repository](https://github.com/smoke-trees/ssr-react) for this post. You can use the repository as the base of your project.

I've added a Dockerfile to create an optimized docker image of your application using the production build.

For consuming API you can use ComponentDidMount() lifecycle method in a class component or useEffect hook in a functional component on the Client side. To consume an API on the server-side please wait for my next post.