Multiple React bundles on a single server Express server (Part-2)

This is the second part of "Multiple React bundles on a single Express server". If you haven't read the first part yet, please go to Multiple React bundles on a single Express server (part 1).

Setup the first React App


After setting up the hosting server, we are ready to set up the first React App. In this setup, we are going to use React, Redux, Babel, and Webpack, etc NPM packages. Due to this sharing might become obsolete after tomorrow, we are going to specify the intended version when installing the NPM packages to avoid the problem of having the trouble in following the example.

Webpack and shared folder


Let us begin by configuring the Webpack and put the sharable code in place.

1. While in the root of the project folder, type following commands at the terminal:
npm i react@16.8.3 react-dom@16.8.3
npm i react-redux@6.0.1 redux@4.0.1 redux-thunk@2.3.0
npm i -D babel-core@6.26.3 babel-loader@7.1.5
npm i -D babel-preset-env@1.7.0 babel-preset-react@6.24.1 babel-preset-stage-0@6.24.1
npm i -D webpack@4.29.3 webpack-cli@3.2.3 terser-webpack-plugin@1.2.3
npm i -D css-loader@2.1.1 html-webpack-plugin@3.2.0 mini-css-extract-plugin@0.5.0
npm i -D node-sass@4.11.0 sass-loader@7.1.0 terser-webpack-plugin@1.2.3

This will install all the important NPM packages to build React App with Webpack.


2. Update the 'scripts' section in package.json to:
"script": {
  "build": "webpack --mode production",
  "client": "webpack --watch --mode development",
  "start": "npm run build && node server/index",
  "test": "echo \"Error: no test specified\" && exit 1"
}

The 'build' script will ask Webpack to bundle the application in production mode. The 'client' script will ask Webpack to bundle the application in development mode. The script 'start' will trigger Webpack build and run the Express server.

Following are the commands to run various scripts:
npm run build
npm run client
npm run start



3. Create the following files and folders for the base configuration:
touch webpack.config.js
mkdir templates
touch templates/index.html
mkdir shared
mkdir shared/components
touch shared/components/ErrorBoundary.js
mkdir shared/state
mkdir shared/state/reducers
touch shared/state/reducers/shareReducers.js
mkdir shared/styles
touch shared/styles/main.css

The command above will create folders and files like the following structure:
 --templates/
    |--index.html
    |
 --shared/
    |--components/
    |   |--ErrorBoundary.js
    |
    |--state/
    |   |--reducers/
    |       |--shareReducers.js
    |
    |--styles/
        |--main.css


4. Add the following code to 'webpack.config.js'
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserPlugin = require('terser-webpack-plugin')

const commonConfig = {
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['env', 'react', 'stage-0']
            }
          }
        ]
      },
      {
        test: /\.(s*)css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader
          },
          'css-loader',
          'sass-loader'
        ]
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  },
  optimization: {
    minimizer: [
      new TerserPlugin ({
        test: /(?!.min).js(\?.*)?$/i,
        sourceMap: true,
        terserOptions: {
          warnings: false,
          comments: false, // drop comments
          compress: {
            pure_funcs: [ 'console.log' ] // drop console.log
          }
        }
      })
    ]
  },
  devtool: 'source-map'
}

const weatherAppConfig = Object.assign({}, commonConfig, {
  entry: {
    app: './weather-app/index.js'
  },
  output: {
    path: path.join(__dirname, './public/weather'),
    filename: 'bundle.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      hash: true,
      title: 'Weather App',
      template: './templates/index.html',
      chunks: ['app'],
      path: path.join(__dirname, './public/weather'),
      filename: 'index.html'
    }),
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: '[id].css'
    })
  ]
})

module.exports = [weatherAppConfig]

The code above is to instruct Webpack how to do the bundling. The 'commonConfig' is the configuration shared by various Apps. The 'weatherAppConfig' is an individual configuration for the Weather App (see below). In short, the configuration is to tell Webpack to bundle the React Apps to the public folder. Each App will have its own folder in the 'Public' folder. All the dependency files such as 'bundle.js', HTML and CSS (from SASS) files will be placed inside the individual folder.



5. Add the following code to 'templates/index.html'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
  <div id="app"></div>
</body>
</html>

The HTML file above is a template used by Webpack to generate the individual 'index.html' files for various apps.



6. Add the following code to 'shared/components/ErrorBoundary.js':
import React from 'react'
export default class ErrorBoundary extends React.Component {
  constructor (props) {
    super(props)
    this.state = { hasError: false }
  }

  componentDidCatch (error, info) {
    // Display fallback UI
    this.setState({ hasError: true })
    // You can also log the error to an error reporting service
    // logErrorToMyService(error, info);
    console.log('ErrorBoundary:\n err: ', error, '\n info: ', info)
  }

  render () {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>
    }
    return this.props.children
  }
}


The code above from the React documentation. It is to help us catch an issue in our React coding. Please refer to https://reactjs.org/docs/error-boundaries.html for more information.



7. Add the following code to 'shared/state/reducers/shareReducers.js':
const initialState = {}

export default function shareReducer (state = initialState, action) {
  const { type, payload = {} } = action
  switch (type) {
    default:
      return state
  }
}

The code above is the reducer to be shared by various React Apps. It is doing nothing useful now.


8. Add the following code to 'shared/styles/main.css':
* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

.grid-container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(6, 1fr);
  grid-gap: 2px;
  padding: 10px;
  width: 100vw;
  height: 100vh;
}

.header-section {
  grid-column: 1 / 4;
  grid-row: 1;
}

.sidebar-section {
  grid-column: 1;
  grid-row: 2 / 7;
}

.main-section {
  grid-column: 2 / 4;
  grid-row: 2 / 6;
}

.footer-section {
  grid-column: 2 / 4;
  grid-row: 6;
}

The code above is the CSS styling to be shared by various React Apps. The code is basically doing Grid layout styling.

The Weather-App

Once the above setup is done, it is time to create the first App. Let assume that the App is about the weather. But we are not getting into the detail of creating the App. The App will just print a simple Hello World message to indicate that it is working.

9. While in the root of the project folder, type following commands at the terminal:
mkdir weather-app
touch weather-app/index.js
mkdir weather-app/components
touch weather-app/components/App.js
mkdir weather-app/state
touch weather-app/state/store.js
mkdir weather-app/state/reducers
touch weather-app/state/reducers/index.js
touch weather-app/state/reducers/mainReducer.js
mkdir weather-app/styles
touch weather-app/styles/local.css

The command above will create folders and files like the following structure:
 --weather-app/
    |--index.js
    |
    |--components/
    |   |--App.js
    |
    |--state/
    |   |--store.js
    |   |--reducers/
    |       |--index.js
    |       |--mainReducer.js
    |
    |--styles/
        |--local.css



10. Add the following code to 'weather-app/index.js':
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import configureStore from './state/store'
import App from './components/App'

const store = configureStore()

document.addEventListener('DOMContentLoaded', () => {
  render(
    <Provider store={store}>
      <App />
    </Provider>,
    document.getElementById('app')
  )
})

The code above is setting up the entry of the Weather App. It also set up the Redux store as the provider.


11. Add the following code to 'weather-app/components/App.js':
import React from 'react'
import ErrorBoundary from '../../shared/components/ErrorBoundary'
import '../../shared/styles/main.css'
import '../styles/local.css'

export default function App (props) {
  return (
    <ErrorBoundary>
      <div className="grid-container">
      <div className='header-section header-bg'><h1>Hello World from Weather App</h1></div>
      <div className='sidebar-section sidebar-bg'>sidebar</div>
      <div className='main-section main-bg'>main content</div>
      <div className='footer-section footer-bg'>footer</div>
      </div>
    </ErrorBoundary>
  )
}

The code above is the root component when React application is running. When the Weather App is successfully built and served across the NodeJS server, you will see the message of "Hello World from Weather App.


12. Add the following code to 'weather-app/state/store.js':
import { createStore, applyMiddleware, compose } from 'redux'
import reduxThunk from 'redux-thunk'
import reducers from './reducers/index.js'

export default function configureStore () {
  return createStore(
    reducers,
    compose(
      applyMiddleware(reduxThunk),
        window.__REDUX_DEVTOOLS_EXTENSION__
          ? window.__REDUX_DEVTOOLS_EXTENSION__()
          : f => f
    )
  )
}

The code above is to set up the Redux store for Weather App.


13. Add the following code to 'weather-app/state/reducers/index.js':
import { combineReducers } from 'redux'
import mainReducer from './mainReducer'
import shareReducer from '../../../shared/state/reducers/shareReducer'

export default combineReducers({
  mainReducer,
  shareReducer
})

The code above is to use combineReducers to link both shared and individual reducers together.


14. Add the following code to 'weather-app/state/reducers/mainReducer.js':
const initialState = {}

export default function mainReducer (state = initialState, action) {
  const { type, payload = {} } = action
  switch (type) {
    default:
      return state
  }
}

The code above is the main reducer for Weather App. It is not doing anything useful now.


15. Add the following code to 'weather-app/syles/local.css':
.header-bg {
  background-color: aquamarine;
}

.sidebar-bg {
  background-color: aqua;
}

.main-bg {
  background-color: #65e2d8;
}

.footer-bg {
  background-color: #55c0ce;
}

The code above is the CSS for the Weather App.



16. Checkpoint: Please do a trial run to see if everything above is done correctly.
npm run start

Then you could use any browser to open the link below:
http://localhost:3000/weather/

If you have done the setup correctly, you should be able to see the message of  'Hello World from Weather App'.

The source code up to this point should be like this:
https://github.com/YWHo/multi-reacts/tree/setup_first_react_app_v2




Next step, we are going to set up the second React APP to show how to create more than one React bundle.
To be continue...

Comments

Popular posts from this blog

Multiple React bundles on a single Express server (Part-1)

100% Test Coverage?