Introduction
This a note on Redux.
Background
Angular, React, and Vue all encourage component based programming. But all share a natural problem on how to handle the shared data and the communication among the components. To address this problem, Redux was introduced. In this note, I will give a small example using Redux with React.
The Node Project & Webpack
The attached is a Node project to serve the content implemented in the "client" directory. While it heavily referenced the project created by the "create-react-app" script, the project is a lot simpler and a lot smaller, so the Webpack and React program can be easily added to the other environments, such as Visual Studio, Maven or Eclipse. The following is the "package.json" file.
{
"name": "redux-example",
"version": "0.0.1",
"private": true,
"scripts": {
"pack-d": "cross-env NODE_ENV=development webpack",
"pack-dw": "cross-env NODE_ENV=development webpack --watch",
"pack-p": "cross-env NODE_ENV=production webpack"
},
"dependencies": {
"express": "4.16.2",
"errorhandler": "1.5.0"
},
"devDependencies": {
"autoprefixer": "7.1.6",
"babel-core": "6.26.0",
"babel-eslint": "7.2.3",
"babel-jest": "20.0.3",
"babel-loader": "7.1.2",
"babel-preset-react-app": "^3.1.2",
"babel-runtime": "6.26.0",
"case-sensitive-paths-webpack-plugin": "2.1.1",
"chalk": "1.1.3",
"css-loader": "0.28.7",
"dotenv": "4.0.0",
"dotenv-expand": "4.2.0",
"eslint": "4.10.0",
"eslint-config-react-app": "^2.1.0",
"eslint-loader": "1.9.0",
"eslint-plugin-flowtype": "2.39.1",
"eslint-plugin-import": "2.8.0",
"eslint-plugin-jsx-a11y": "5.1.1",
"eslint-plugin-react": "7.4.0",
"extract-text-webpack-plugin": "3.0.2",
"file-loader": "1.1.5",
"fs-extra": "3.0.1",
"html-webpack-plugin": "2.29.0",
"object-assign": "4.1.1",
"postcss-flexbugs-fixes": "3.2.0",
"postcss-loader": "2.0.8",
"promise": "8.0.1",
"raf": "3.4.0",
"react": "^16.5.0",
"react-dev-utils": "^5.0.2",
"react-dom": "^16.5.0",
"react-redux": "5.0.7",
"redux": "3.5.2",
"resolve": "1.6.0",
"style-loader": "0.19.0",
"sw-precache-webpack-plugin": "0.11.4",
"url-loader": "0.6.2",
"webpack": "3.8.1",
"webpack-manifest-plugin": "1.3.2",
"whatwg-fetch": "2.0.3",
"cross-env": "5.2.0"
},
"babel": {
"presets": [
"react-app"
]
},
"eslintConfig": {
"extends": "react-app"
}
}
The project uses Webpack to bundle the ES6 React code, the following is the "webpack.config.js" file.
'use strict';
const autoprefixer = require('autoprefixer');
const path = require('path');
const webpack = require('webpack');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const eslintFormatter = require('react-dev-utils/eslintFormatter');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const paths = {
publicPath: '/build/',
outputPath: path.resolve('./client/build'),
polyfills: path.resolve('./client/src/polyfills'),
index: path.resolve('./client/src/index.js'),
appNodeModules: path.resolve('./node_modules'),
appSrc: path.resolve('./client/src'),
appPackageJson: path.resolve('./package.json'),
};
module.exports = {
devtool: 'cheap-module-source-map',
entry: { index: [ paths.polyfills, paths.index ]},
output: {
pathinfo: true,
path: paths.outputPath,
filename: '[name].js',
chunkFilename: '[name].chunk.js',
publicPath: paths.publicPath
},
resolve: {
modules: ['node_modules'],
extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'],
alias: { 'react-native': 'react-native-web' },
plugins: [ new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]) ],
},
module: {
strictExportPresence: true,
rules: [
{
test: /\.(js|jsx|mjs)$/, enforce: 'pre',
use: [
{
options: {
formatter: eslintFormatter,
eslintPath: require.resolve('eslint'),
},
loader: require.resolve('eslint-loader'),
},
], include: paths.appSrc,
},
{
oneOf: [
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]',
},
},
{
test: /\.(js|jsx|mjs)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: { cacheDirectory: true, },
},
{
test: /\.css$/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
'>1%',
'last 4 versions',
'Firefox ESR',
'not ie < 9',
],
flexbox: 'no-2009',
}),
],
},
},
],
},
{
exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/],
loader: require.resolve('file-loader'),
options: { name: 'static/media/[name].[hash:8].[ext]', },
},
],
},
],
},
plugins: [
new webpack.NamedModulesPlugin(),
new CaseSensitivePathsPlugin(),
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
}),
],
node: { dgram: 'empty', fs: 'empty',
net: 'empty', tls: 'empty', child_process: 'empty', },
performance: { hints: false, },
};
if (process.env.NODE_ENV === 'production') {
module.exports.plugins = (module.exports.plugins || []).concat([
new webpack.optimize.UglifyJsPlugin({})])}
If you get an warning for the 'production' bundle, you can add the "webpack.DefinePlugin" to the "webpack.config.js" file.
if (process.env.NODE_ENV === 'production') {
module.exports.plugins = (module.exports.plugins || []).concat([
new webpack.optimize.UglifyJsPlugin({}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
})]);
}
The Node application's entry point is the "app.js" file.
var express = require('express'),
http = require('http'),
path = require('path'),
errorhandler = require('errorhandler');
var app = express();
app.set('port', 3000);
app.use(function (req, res, next) {
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
res.header('Expires', '-1');
res.header('Pragma', 'no-cache');
next();
});
app.use(express.static(path.join(__dirname, 'client')));
app.use(errorhandler());
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
To run the application, you need to first transpile and pack the React program into a bundle. You can use the following command to create a development bundle.
npm run pack-d
If you want to set a watcher to regenerate the bundle whenever you save the related files, you can use the following command:
npm run pack-dw
If for some reason the watcher does not work or quit without any message, you can add the following entry to the "webpack.config.js" file.
watchOptions: {
poll: true
}
You can use the following command to create a production bundle, which takes a longer time to generate but has a much smaller bundle size.
npm run pack-p
After generating the bundles, you can start the Node server to serve the application.
node app.js
Redux & Actions & Reducers & Connect & Provider & React
This is an example of Redux in React, where Redux is a framework to share date and communicate among the React components. The Redux has three principles:
- Single source of truth - the data is stored in an object tree inside a single store
- State is read-only - the only way to mutate the state is to emit an action, an object describing what happened
- Mutations are created by pure functions - the functions are called reducers
This note is about Redux but not React. If you are not familiar with React, you can take a look at my earlier notes. To use Redux with React, you will need the "redux" and "react-redux" npm packages. A typical Redux React application will have the following building blocks:
- Actions - the helper functions to generate Redux events
- Reducers - the functions to handle the events and return the appropriate state entry by the type of events
- Components - the React components that are connected with Redux so they can dispatch events and get notified by the state changes
- The store and the provider - a store needs to be created by the reducers and associated with a provider so it can communicate with the components
With the Redux concepts in mind, let us take a look at the concrete example implementation with React.
The Actions
export const addNumberEvent = number => ({
type: 'ADD_NUMBER',
payload: number
});
export const clearNumbersEvent = () => ({
type: 'CLEAR_NUMBERS'
});
The actions are helper functions to generate action objects. A typical action object has a type and a payload property. The "addNumberEvent
" creates an action object to instruct the reducer to add a number to the list in the data store. The "clearNumbersEvent
" creates an action object to instruct the reducer to clear the list of numbers. The action objects are processed by the reducers.
The Reducers & the Combined Reducer
import { combineReducers } from 'redux';
const numbers = (state = [], action) => {
switch (action.type) {
case 'ADD_NUMBER':
return [
...state,
action.payload
];
case 'CLEAR_NUMBERS':
return state.length === 0? state: [];
default:
return state;
}
};
export default combineReducers({ numbers });
- A reducer function takes two parameters, the state entry and the action object.
- It is important that the state entry has a default value, it is the initial value of the state entry in the Redux store object.
- The reducer function mutates the state entry based on the information in the action object.
- If no action is taken, the reducer function should not alter the state entry but simply return it back to the caller.
- In a typical Redux application, we may have multiple reducers. It is important to export the "
combineReducers
". - The Redux framework uses the information in the reducers combined to create the initial state of the store. The property name of each data entry in the Redux store corresponds to the function name of each reducer.
The React Components & the Connected Components
import React from 'react';
import PropTypes from 'prop-types';
import { addNumberEvent, clearNumbersEvent } from '../actions';
import { connect } from 'react-redux';
const Commander = ({ onClick, ownProps, children }) => (
<button onClick={onClick} className="commander">{children}</button>
);
Commander.propTypes = {
onClick: PropTypes.func.isRequired
};
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => {
switch (ownProps.command) {
case 'ADD_NUMBER':
let number = Math.floor((Math.random() * 100) + 1);
dispatch(addNumberEvent(number));
break;
case 'CLEAR_NUMBERS':
dispatch(clearNumbersEvent());
break;
default:
return;
}
}
});
export default connect(null, mapDispatchToProps)(Commander);
- The "
Commander
" is a standard React component, which raises a click event through the "onClick
" React "prop
"; - The "
mapDispatchToProps
" function takes a parameter "dispatch
" and uses it to send the appropriate action object to the reducer functions; - It is important to call the "
connect
" function and export the connected component, so the Redux framework can communicate with the component.
The "Commander
" component dispatches the actions to mutate the data in the Redux store, while the "ItemAggregator
" and "ItemList
" components display the data.
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
const ItemAggregator = ({ sum }) => (
<div className="ItemAggregator">Total SUM - {sum}</div>
);
ItemAggregator.propTypes = {
sum: PropTypes.number.isRequired
};
const mapStateToProps = (state) => {
return {
sum: state.numbers.reduce((a, b) => a + b, 0)
};
};
export default connect(mapStateToProps)(ItemAggregator);
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
const ItemList = ({ items }) => (
<div>
<div className="ItemList">
{items.map((item, i) =>
<div key={i}>No.{i + 1} - {item}</div>
)}
</div>
</div>
);
ItemList.propTypes = {
items: PropTypes.arrayOf(PropTypes.number).isRequired
};
const mapStateToProps = (state) => {
return {
items: state.numbers
};
};
export default connect(mapStateToProps)(ItemList);
- The "
mapStateToProps
" function takes the state
object from the store and transforms it to the format required by the React component; - Both the "
mapDispatchToProps
" and "mapStateToProps
" functions need to return an object that has entries that match the name of the "props
" expected by the React component; - Both React components are connected so the Redux framework can deliver the state changes to them.
Display on the UI & the Store & the Provider
import React from 'react';
import Commander from './Commander';
import ItemAggregator from './ItemAggregator';
import ItemList from './ItemList';
const App = () => (
<div className="container">
<Commander command='ADD_NUMBER'>Add a random number</Commander>
<Commander command='CLEAR_NUMBERS'>Clear the numbers</Commander>
<ItemList />
<ItemAggregator />
</div>
);
export default App;
The "App
" component is bound to the UI in the "index.js" file.
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import reducers from './reducers'
import App from './components/App';
import './styles/app-style.css';
const store = createStore(reducers);
ReactDOM.render(<Provider store={store}><App /></Provider>,
document.getElementById('root'));
- To use Redux, we need to create a store based on the combined reducers.
- The stored needs to be assigned to a "
Provider
" component and the other components who share the store need to be the children of the "Provider
" component.
Now we completed a simple React application with Redux. It may seem a little too complicated for the kind of work that we want to do, but when your application gets larger, you will see the benefit from Redux. If you take a look back at what we have done, you should notice that the components and the Redux reducers can be developed independently but simply connected to each other through the Redux framework to nicely share data and communicate with each other.
Run the Application
If you transpile and bundle your React package and start your node server, you can run the application by "http://localhost:3000/
".
You can click the "Add..." button to add a random number to the list and you can clear the numbers by the "Clear..." button. Although none of the React component actually holds the data, they share the data in the store and communicate nicely by the Redux framework.
Points of Interest
- This a note on Redux.
- While Redux is a nice framework, it certainly has some rooms for improvement for simplicity and performance and scalability.
- I hope you like my postings and I hope this note can help you one way or the other.
History
- 9/21/2018: First revision