Tutorials‎ > ‎

React hot reload & Redux (Part 1)

posted Nov 30, 2016, 10:45 PM by Denny Angkasa   [ updated Dec 5, 2016, 6:55 PM by Surya Wang ]

This tutorial will cover how to setup react hot reload with redux. React hot reload allows us to change the code live without the the page being refreshed, unlike webpack-dev-server that auomatically refreshes the page when the code is changed, hot reload allows the page to load the changes on the fly, preserving the state of the application. We will also set up Redux in this tutorial, using it to manage the state of the application.

We start by creating the file package.json
{
  "name": "reactest",
  "version": "1.0.0",
  "description": "this is a test application",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "author": "Denny Angkasa",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.18.2",
    "babel-loader": "^6.2.8",
    "babel-preset-es2015": "^6.18.0",
    "babel-preset-react": "^6.16.0",
    "express": "^4.14.0",
    "react-hot-loader": "^1.3.1",
    "webpack": "^1.13.3",
    "webpack-dev-middleware": "^1.8.4",
    "webpack-hot-middleware": "^2.13.2"
  },
  "dependencies": {
    "react": "^15.4.1",
    "react-dom": "^15.4.1",
    "react-redux": "^4.4.6",
    "redux": "^3.6.0"
  }
}


We will use the file to manage our dependencies and use scripts for npm. We are dividing the dependencies into 2, devDependencies and dependencies. DevDependencies are dependencies that will be used only in development, while dependencies are needed to execute the application. Babel here will be the transpiler for our JSX & ES6 code to ES5 javascript code. Express will be used as our server to serve static files, with webpack middlewares as the API for the node server to enable hot reloading. Webpack is used to compile our javascript codes into bundles. When this package.json has been created, open a command window in the directory of the package.json file and execute npm install. The package manager will install our dependencies.


Next we will create the node server to serve our static files and hot reloading. create the file server.js. This server will listen on port 3000. As we can see in the server.js, the server needs webpack config and index.html.
var path = require('path');
var webpack = require('webpack');
var express = require('express');
var config = require('./webpack.config');

var app = express();
var compiler = webpack(config);

app.use(require('webpack-dev-middleware')(compiler, {
  publicPath: config.output.publicPath
}));

app.use(require('webpack-hot-middleware')(compiler));

app.get('*', function(req, res) {
  res.sendFile(path.join(__dirname, 'index.html'));
});

app.listen(3000, function(err) {
  if (err) {
    return console.error(err);
  }

  console.log('Listening at http://localhost:3000/');
})


Let's create the index.html file.
<html>
  <head>
    <meta charset="utf-8">
    <title>React Application</title>
  </head>
  <body>
    <div id="app" />
    <script src="static/bundle.js" type="text/javascript"></script>
  </body>
</html>
The index.html file expects only a single javascript file, which is the bundle.js in static folder bundled by webpack.


Next will be the webpack.config.js. This is the main configuration file for webpack and is very important for the hot reload to work.
var webpack = require('webpack');
var path = require('path');

var BUILD_DIR = path.resolve(__dirname, 'dist');
var APP_DIR = path.resolve(__dirname, 'src');

var config = {
    devtool: 'inline-source-map',
    entry: [
        'webpack-hot-middleware/client',
        APP_DIR + '/index.jsx'
    ],
    output: {
        path: BUILD_DIR,
        filename: 'bundle.js',
        publicPath: '/static/'
    },
    module: {
        loaders: [
            {
                test: /\.jsx?/,
                include: APP_DIR,
                loaders: ['react-hot', 'babel']
            }
        ]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoErrorsPlugin
    ]
};

module.exports = config;
In this file we define the directories for build and development source files. We use the inline-source-map devtool to provide easy debugging. Because when javascript files are bundled it is very difficult to debug it without source maps. Inline source map allows us to easily debug our application using browser's development tools as if we are actually debugging our source code without bundling. Entries are used for the webpack entry into our application, and the webpack-hot-middleware/client is used by the server to provide our hot reload. Here we are entering our application through the file index.jsx. The output will be the target location for webpack bundling output, with the filename bundle.js. Modules are used to define which loaders we will used for each file type. Here we state using regex that *.js and *.jsx files will use react-hot loader and babel loader. To enable hot reload we have to use the plugin webpack.HotModuleReplacementPlugin(), while webpack.NoErrorsPlugin ensures that our application won't crash and need to reload when we do syntax error in our code. We then export the config for the server.js to use.


Note that above we use babel loader. We need to create the babel configuration file. Create the file .babelrc
{
    "presets": ["react", "es2015"]
}


Now we have finished configuring our application. Next we will configure our redux components. 2 of the main components of redux are actions and reducers. Actions are functions that doesn't directly alter the application's states. Reducers on the other hand will be the one that updates the state of the application using actions.
Create the folder src in our project folder. Inside the src folder create actions folder. The actions folder will contain all of our actions. In the actions folder create the file index.js.
export const ADD_PRODUCT = 'ADD_PRODUCT';
export const UPDATE_PRODUCT = 'UPDATE_PRODUCT';
export const DELETE_PRODUCT = 'DELETE_PRODUCT';

let nextProductId = 1;
export function addProduct(product)
{
    return {
        type: ADD_PRODUCT,
        product: {
            id: nextProductId++,
            name: product.name,
            description: product.description
        } 
    };
}
For now we will only implement the AddProduct functionality in the action. As you can see, this action will only return a new product with unique id each time it is called, hence not updating the state of the application.


Now we will create the reducer for products. Create the folder reducers in the same directory as actions folder. Inside it we create products.js containing the reducer for products.
import { ADD_PRODUCT, UPDATE_PRODUCT, DELETE_PRODUCT } from '../actions';

const initialState = {
    products: []
};

export function crudApp(state = initialState, action) {

    switch(action.type)
    {
        case ADD_PRODUCT: {
            return Object.assign({}, state, {
                products: [
                    ...state.products,
                    action.product
                ]
            });
        }
    }

    return state;
}
Which is the ES6 equivalent of
import { ADD_PRODUCT, UPDATE_PRODUCT, DELETE_PRODUCT } from '../actions';

const initialState = {
    products: []
};

export function crudApp(state, action) {
    
    if(typeof state === 'undefined')
        state = initialState;

    switch(action.type)
    {
        case ADD_PRODUCT: {
            return Object.assign({}, state, {
                products: [
                    ...state.products,
                    action.product
                ]
            });
        }
    }

    return state;
}


Here we import the action strings from the actions index.js file. We create an initial state of the application, containing empty array of products. This reducer will export the crudApp function that will be used to update the state of the application. When we call this function, we have to pass a state and an action string. For now we will only handle the action for adding a new product. Notice that we are returning a new state if the action is valid, without overriding the passed state.  This allows us to preserve all the state that the application has been in. An example in the ADD_PRODUCT action, we append the products with the new product created by the action.


Now we are gonna create the entry point and the main file of our application. Create the file index.jsx in the same directory as actions and reducers folder.
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';

import { crudApp } from './reducers/products';

import { addProduct } from './actions';

var store = createStore(crudApp);

class App extends React.Component {

    constructor(){
        super();
        this.state = {
            products: [],
            productName: '',
            productDescription: ''
        };
    }

    componentWillMount(){
        store.subscribe(() => {
            var state = store.getState();
            this.setState({
                products: state.products
            });
        });
    }

    handleAddProductClick()
    {
        store.dispatch(addProduct({
            name: this.state.productName,
            description: this.state.productDescription
        }));
    }

    handleProductNameChange(pn){

        this.setState({
            products: this.state.products,
            productName: pn.target.value,
            productDescription: this.state.productDescription
        });
    }

    handleProductDescriptionChange(pd){
        this.setState({
            products: this.state.products,
            productName: this.state.productName,
            productDescription: pd.target.value
        });
    }

    render () {
        return(
            <div>
                <p>HMR is cool</p>
                <table>
                    <tbody>
                        <tr>
                            <td>Product Name</td>
                            <td><input type="text" onChange={this.handleProductNameChange.bind(this)}/></td>
                        </tr>
                        <tr>
                            <td>Product Description</td>
                            <td><input type="text" onChange={this.handleProductDescriptionChange.bind(this)}/></td>
                        </tr>
                    </tbody>
                </table>
                <button onClick={this.handleAddProductClick.bind(this)}>Add product</button>
                <ul>
                {
                    this.state.products.map((product, i) => {
                        return <li key={i}>{product.name} - {product.description}</li>
                    })
                }
                </ul>
            </div>
        );
    }
};

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

module.hot.accept();
Here we create store by using the createStore method from redux, passing in our crudApp function exported by reducers/products.js. On componentWillMount we subscribe to the store and state what to do when the state is updated. When the state is updated we update the state of the component that will update the view. To run the application just open a command window in the project root folder and execute npm start.


Every time we update the code the HMR will hot reload our application, preserving the state of applications that are not in the files we changed.

Directory structure

- node_modules
- src
    - actions
        - index.js
    - reducers
        - products.js
    - index.jsx
- .babelrc
- index.html
- package.json
- server.js
- webpack.config.js

ċ
reactupload.rar
(3k)
Denny Angkasa,
Dec 1, 2016, 12:29 AM
Comments