Dev

By Carlos Santana on
Reading time: 10 minutes

Webpack-ytfX9.png

In the last post we implemented Webpack Dev Server to add some styles to our project, now we are going to implement React using Node.js and Webpack 4, this will help you to have more robust applications.

First, you need to install all these packages:

npm install @babel/core @babel/node @babel/preset-env @babel/preset-react express nodemon react-hot-loader react-router-dom webpack-hot-middleware compression-webpack-plugin

Implementing React Hot Loader and Webpack in Node

Open your .babelrc and include the react-hot-loader plugin just for development: 

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ],
  "env": {
    "development": {
      "plugins": [
        "react-hot-loader/babel"
      ]
    }
  }
}
File: .babelrc

Now, let's create an Express Server; you need to create a file at src/backend/index.js:

// Dependencies
import express from 'express';
import path from 'path';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpack from 'webpack';

// Webpack Configuration
import webpackConfig from '../../webpack.config.babel';

// Client Render
import clientRender from './render/clientRender';

// Environment
const isProduction = process.env.NODE_ENV === 'production';

// Express Application
const app = express();

// Webpack Compiler
const compiler = webpack(webpackConfig);

// Webpack Middleware
if (!isProduction) {
  // Hot Module Replacement
  app.use(webpackDevMiddleware(compiler));
  app.use(webpackHotMiddleware(compiler));
} else {
  // GZip Compression just for Production
  app.get('*.js', (req, res, next) => {
    req.url = `${req.url}.gz`;
    res.set('Content-Encoding', 'gzip');
    next();
  });
}

// Public directory
app.use(express.static(path.join(__dirname, '../../public')));

// Client Side Rendering
app.use(clientRender());

// Disabling x-powered-by
app.disable('x-powered-by');

// Listen Port 3000...
app.listen(3000);
File: src/backend/index.js

In the last posts, we were using the html-webpack-plugin package to render the initial HTML template; now we have to do that with Node.js. For this, we need to create the file src/backend/render/html.js

// Environment
const isProduction = process.env.NODE_ENV === 'production';

export default function html({ title }) {
  let path = '/';
  let link = '';

  if (isProduction) {
    path = '/app/';
    link = `<link rel="stylesheet" href="${path}css/main.css" />`;
  }

  return `
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8"> <title>${title}</title> ${link}
      </head>

      <body>
        <div id="root"></div>

        <script src="${path}vendor.js"></script>
        <script src="${path}main.js"></script>
      </body>
    </html>
  `;
}
File: src/backend/render/html.js

We need to create a function to render the HTML; I called this clientRender.js:

import html from './html';

export default function clientRender() {
  return (req, res) => res.send(html({
    title: 'DEV Education'
  }));
}
File: src/backend/render/clientRender.js

After we have created our server files, we have to add our main entry file for the client. In this file, we are going to wrap our main App component inside the React Hot Loader (AppContainer):

// Dependencies
import React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';

// Components
import App from './frontend/App';

// Root element
const rootElement = document.querySelector('#root');

// App Wrapper
const renderApp = Component => {
  render(
    <AppContainer>
      <Component />
    </AppContainer>,
    rootElement
  );
};

// Rendering app
renderApp(App);

// Hot Module Replacement
if (module.hot) {
  module.hot.accept('./frontend/App', () => {
    renderApp(require('./frontend/App').default);
  });
}
File: src/index.jsx

Let's create a directory for our frontend files. The first file we need to create is the App.jsx where we are going to include the component's routes: 

// Dependencies
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';

// Components
import About from './components/About';
import Home from './components/Home';

const App = () => (
  <BrowserRouter>
    <Switch>
      <Route exact path="/" component={Home} />
      <Route exact path="/about" component={About} />
    </Switch>
  </BrowserRouter>
);

export default App;
File: src/frontend/App.jsx

To test the routes you need to create the About component: 

import React from 'react';
import styles from './About.scss';

const About = () => <h1 className={styles.About}>About component</h1>;

export default About;
File: src/frontend/components/About/index.jsx

Adding some basic styles: 

.About {
  color: red;
}
File: src/frontend/components/About/About.scss

React Hot Loader is useful to refresh the page every time we make a change in our code. We need to do some changes in our Webpack configuration and add an entry for the webpack-hot-middleware and one for the react-hot-loader to enable the HMR (Hot Module Replacement): 

const isProduction = process.env.NODE_ENV === 'production';
const entry = [];

if (!isProduction) {
  entry.push(
    'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr&reload=true',
    'react-hot-loader/patch',
    './src/index.jsx'
  );
} else {
  entry.push('./src/index.jsx');
}

export default entry;
File: webpack/configuration/entry.js

After you need to create the output.js file to specify where our Webpack should save the files: 

// Dependencies
import path from 'path';

export default {
  filename: '[name].js',
  path: path.resolve(__dirname, '../../public/app'),
  publicPath: '/'
};
File: webpack/configuration/output.js

You need to create also the mode.js file, and handle the environment mode from our JS file because we are going to change our start script and we won't specify the mode directly anymore:

const isProduction = process.env.NODE_ENV === 'production';

export default !isProduction ? 'development' : 'production';
File: webpack/configuration/mode.js

Add the HotModuleReplacementPlugin into the plugins file for development and CompressPlugin for production:

import webpack from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import WebpackNotifierPlugin from 'webpack-notifier';
import CompressionPlugin from 'compression-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';

const isProduction = process.env.NODE_ENV === 'production'

const plugins = [];

if (isProduction) {
  plugins.push(
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css'
    }),
    new CompressionPlugin({
      filename: '[path].gz[query]',
      algorithm: 'gzip',
      test: /.js$/
    })
  );
} else {
  plugins.push(
    new webpack.HotModuleReplacementPlugin(),
    new BundleAnalyzerPlugin(),
    new WebpackNotifierPlugin({
      title: 'DEV Education'
    })
  )
}

export default plugins;
File: webpack/configuration/plugins.js

You need to import all these files into the index.js inside the configuration directory:

// Configuration
import devtool from './devtool';
import entry from './entry';
import mode from './mode';
import module from './module';
import optimization from './optimization';
import output from './output';
import plugins from './plugins';
import resolve from './resolve';

export {
  devtool,
  entry,
  mode, 
  module,
  optimization,
  output,
  plugins,
  resolve
};
File: webpack/configuration/index.js

Finally, in the package.json the new start script should be like this: 

  "scripts": {
    "build": "NODE_ENV=production webpack",
    "clean": "rm -rf public/app",
    "start": "npm run clean && NODE_ENV=development nodemon src/backend --watch src/backend --exec babel-node --presets @babel/preset-env",
    "start:production": "npm run clean && npm run build && NODE_ENV=production babel-node src/backend --presets @babel/preset-env"
  }
File: package.json

TIP: If you use Windows, you have to use the SET keyword to specify NODE_ENV, for example, SET NODE_ENV=development or SET NODE_ENV=production otherwise won't work in your computer. I recommend you to read the post: The most common problems using React on Windows.

Testing our application

If you did everything correctly, then you should be able to run the application by running the command: npm start

HomeWithNode-vsXyF.png

Now if you inspect the site and you see the console, you will see something like this:

HMR-6DcOs.png

As you there is a warning from React-Hot-Loader that says react-dom is not detected. To fix this first you need to install this package:

npm install @hot-loader/react-dom

Then you need to add an alias in your resolve.js file: 

Only members can see all the codes
You can Login or Sign Up

File: webpack/configuration/resolve.js

This will use the @hot-loader/react-dom instead of the original react-dom, basically is the same, but includes the necessary patch to be able to work with the newest features of React 16.6+. Now the warning will disappear: 

HMRNoWarning-4ZSgX.png

Now if you want to test that the HMR actually works, then go and modify the Home component, I added an extra text "- Updated", and once I saved automatically the page got updated (without refresh the page):

UpdatingHMR-mqntY.png

Also if you change the style of About.scss (change the color to green), you will see that automatically will update: 

About-60kkG.png

This is great!, now we connected our HMR successfully and works like a charm!

Another thing we need to see is that we are injecting 2 bundles, the vendor.js, and the main.js.

Bundles-orIHi.png

As you can see the big size of our bundles (vendor.js is 1.3Mb and main.js 46.3Kb).

If you want to run your application in production mode then execute npm run start:production. If everything works fine, you should see the same site but with smaller bundles: 

BundlesGZip-IYnW4.png

Awesome!, now our vendor.js is only 42.8Kb and the main.js is only 1.3Kb!!!

Wow, this was a long post, but I think was very useful and I hope you learned a lot of new things. If you want to learn more about React and Webpack, then you should buy my React Cookbook!

Git Repository: https://github.com/D3vEducation/node-with-react

avatarLeave a comment

Your comment

Only members can comment. You can Login or Sign Up