{EP}

David Perez

Setting Up a Modern Webapp 2020 Edition Part II

JavaScript

September 28, 2020

Overview

For this part, we'll focus on setting up the react family of libraries. This includes react itself, redux, redux sagas, immutable, and react router. You can find the code here.

The Setup

The setup is somewhat opinionated, but the main idea is to keep it flexible, best practices, and modern. It will include the following:

  • React: UI Library.
  • React-Router: UI route manager.
  • Redux: State management.
  • Redux-dom: React to html converter.
  • Redux-Sagas: Asynchronous data management.
  • Immutable: Allows for working with immutable data.

Step 1: Installing dependencies

We'll install all of the react libraries mentioned above. Basically, we have to do two steps and it is to install the required babel libraries and then the actual libraries we are using. Babel needs to be able to parse react or JSX, also to be able to parse generators since we'll be using these with our sagas

npm i -D \
@babel/core@7.10.3 \
@babel/plugin-transform-runtime@7.10.4 \
@babel/preset-env@7.10.3 \
@babel/preset-react@7.10.1 \
@babel/runtime@7.10.4 \
babel-loader@8.1.0 \
immutable@4.0.0-rc.12 \
react@16.13.1 \
react-dom@16.13.1 \
react-redux@7.2.0 \
react-router-dom@5.2.0 \
redux@4.0.5 \
redux-immutable@4.0.0 \
redux-saga@1.1.3

Step 2: React configuration

Let's start with the configuring babel. This way when we add react, everything will work as expected.

# Create the .babelrc file for bable configuration
cd PROJECT_DIR
touch .babelrc

Our .babelrc file should look like the following:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": ["@babel/transform-runtime"]
}

Next is to update our webpack.config.js file to be able to run js files through babel and our custom configuration.

Here is the updated webpack.config.js file:

const { join } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const htmlConfig = new HtmlWebpackPlugin({
  title: 'Web App 2020',
  template: './index.ejs'
})

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      }
    ]
  },
  resolve: {
    alias: {
      components: join(__dirname, 'src/components'),
      containers: join(__dirname, 'src/containers')
    }
  },
  plugins: [htmlConfig],
  devtool: 'inline-source-map'
}

Now that we have babel + webpack working together and supporting react, we are able to start adding some react to our app. Let's start making some directories and files for our react setup.

cd PROJECT_DIR

# Create a components dir (simple / dumb components)
mkdir src/components

# Create a containers dir (smart / higher order level components - HOC)
mkdir src/containers

# Our redux / redux-sagas files
touch src/store.js
touch src/reducers.js
touch src/sagas.js

# Simple component
mkdir src/components/button/

Routing

To start, we'll set up the routing for our app using react-router-dom. You can find additional configs on their website. We'll do some simple routing for this app.

Modify your src/index.js file to contain the following:

import React from 'react'
import { render } from 'react-dom'
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about">About</Link>
            </li>
          </ul>
        </nav>

        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/users">
            <Users />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  )
}

function Home() {
  return <h2>Home</h2>
}

function About() {
  return <h2>About</h2>
}

function Users() {
  return <h2>Users</h2>
}

render(<App />, document.getElementById('app'))

You should be able to run the app with npm start and see the following in your browser:

React Router Success

Clicking to the other links should take you to the different "pages".

Redux

Redux has gotten a bad rep due to some complexity in configuration, hopefully we'll make this process less painful. To Use redux, we'll need some "smart" components which are basically regular components but they hook up to redux to get their props. For this setup, we'll configure redux and create an action which toggles the menu.

Here is the over all structure:

New file structure

Let's create the following files that we'll need for the redux config.

src/index.js Modifed this file and moved most of its contents to the app container

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from 'containers/app/app'
import configureStore from './store'

const store = configureStore({})

// To easily access store from chrome dev tools
window.store = store

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

src/store.js This file configures redux store.

import { createStore, applyMiddleware, compose } from 'redux'
import rootReducer from './reducers'

export default function configureStore(initialState) {
  const middlewares = []
  const middlewareEnhancer = applyMiddleware(...middlewares)
  const enhancers = [middlewareEnhancer]
  const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

  const store = createStore(
    rootReducer(),
    initialState,
    composeEnhancer(...enhancers)
  )

  return store
}

src/reducers.js This file combines all of the different reducers in our application.

import { combineReducers } from 'redux'
import config from 'containers/app/reducer'

const rootReducer = (history) =>
  combineReducers({
    config
  })

export default rootReducer

src/containers/app/actions.js This file contains the redux actions and action creators.

export const TOGGLE_MENU = 'app/TOGGLE_MENU'

export function toggleMenu() {
  return { type: TOGGLE_MENU }
}

src/containers/app/reducer.js This file contains the reducers for app container.

import { TOGGLE_MENU } from './actions'

const initialState = {
  menuOpened: true
}

export default function config(state = initialState, action) {
  switch (action.type) {
    case TOGGLE_MENU: {
      const newState = { ...state, menuOpened: !state.menuOpened }

      return newState
    }
    default:
      return state
  }
}

src/containers/app.js This file is the main container for the application.

import React from 'react'
import { render } from 'react-dom'
import { connect } from 'react-redux'
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'
import { toggleMenu } from './actions'

function Home() {
  return <h2>Home</h2>
}

function About() {
  return <h2>About</h2>
}

function Users() {
  return <h2>Users</h2>
}

export const App = ({ toggleMenu, menuOpened }) => {
  let menuHTML

  if (menuOpened) {
    menuHTML = (
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/users">Users</Link>
          </li>
        </ul>
      </nav>
    )
  }

  return (
    <Router>
      <div>
        {menuHTML}

        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/users">
            <Users />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>

        <button onClick={() => toggleMenu()}>Toggle Menu</button>
      </div>
    </Router>
  )
}

const mapStateToProps = (state) => ({
  menuOpened: state.config.menuOpened
})

const mapDispatchToProps = {
  toggleMenu
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

webpack.confg.js We tweaked this file to make it easier to get components and containers.

So, whenever we want a component we would do import Button from 'components/button' instead of having to use relative paths.

const { join } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const htmlConfig = new HtmlWebpackPlugin({
  title: 'Web App 2020',
  template: './index.ejs'
})

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },
      {
        test: /\.(png|jpe?g|gif)$/i,
        use: [
          {
            loader: 'file-loader'
          }
        ]
      }
    ]
  },
  resolve: {
    alias: {
      components: join(__dirname, 'src/components'),
      containers: join(__dirname, 'src/containers')
    }
  },
  plugins: [htmlConfig],
  devtool: 'inline-source-map'
}

With this we have finalized our Redux setup. By now, we should have routing and redux configured. You should be able to toggle the menu.

Menu Opened Menu Closed

In the next part, we'll add immutability to our app and also add the routing into our redux store so we have one source of truth.