Introduction

Since their launch just a few years ago, React and Redux have by now taken the web by storm. React.JS is one of the most commonly used web frontend technologies, especially popular among top web properties.

Redux was originally developed as a React.JS state-management solution, inspired by Facebook’s own Flux architecture, as well as functional programming language Elm. Currently, the use of Redux reaches far beyond managing state of React.JS applications. In addition to React and React-Native, Redux stores are commonly utilized by developers building front-end applications in Angular and Vue.JS, and sometimes even to manage the state of backend applications built with Node.JS.

How Redux works

At the core of Redux are pure JavaScript functions called reducers. A reducer (a name derived from the JavaScript reduce method) that takes two parameters: an action, and a previous state returning the next state. For applications with complex states, Redux allows separating related actions and parts of the state into slices, then using redux combineReducers() method to join individual slices and their reducers into a unified state.

Implementing Redux state management comes down to several simple tasks:

Defining actions dispatched during load and save operations

Implementing reducer reacting to each action taken by appropriately updating the redux state or the state’s slice

Connecting Redux state to React components so that redux state objects and load/save functions are available within the component.

Lastly, Redux supports the concept of a middleware, similar to the one used by Express.JS. Redux middleware is a layer providing a third-party extension point between dispatching an action, and the moment it reaches the reducer. Middleware components are commonly used for things like logging, crash reporting and more. For more information about Redux please view the official documentation.

Let us implement a complete Redux reducer, below.

const requestAdvertizers = () => ({ type: 'REQUEST_ADVERTIZERS' }); const receiveAdvertizers = response => ({ type: 'RECEIVE_ADVERTIZERS', payload: response }); const receiveAdvertizersError = err => ({ type: 'RECEIVE_ADVERTIZERS_ERROR', error: true, payload: err }); export const loadAdvertizers = () => (dispatch, getState) => { dispatch(requestAdvertizers()); const url = '/api/advertizers'; return get(getState, url) .then(response => { dispatch(receiveAdvertizers(response.data)); }) .catch(err => dispatch(receiveAdvertizersError(err))); } const initialState = { loading: false, loaded: false, error: false, advertizers: null, } export const reducer = (state = initialState, action) => { switch (action.type) { case 'REQUEST_ADVERTIZERS': { return Object.assign({}, state, { loading: true, error: false }); } case 'RECEIVE_ADVERTIZERS_ERROR': { return Object.assign({}, state, { error: true, loading: false, loaded: true }); } case 'RECEIVE_ADVERTIZERS': { return Object.assign({}, state, { error: false, loading: false, loaded: true, advertizers: action.payload.data }); } default: return state; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 const requestAdvertizers = ( ) = > ( { type : 'REQUEST_ADVERTIZERS' } ) ; const receiveAdvertizers = response = > ( { type : 'RECEIVE_ADVERTIZERS' , payload : response } ) ; const receiveAdvertizersError = err = > ( { type : 'RECEIVE_ADVERTIZERS_ERROR' , error : true , payload : err } ) ; export const loadAdvertizers = ( ) = > ( dispatch , getState ) = > { dispatch ( requestAdvertizers ( ) ) ; const url = '/api/advertizers' ; return get ( getState , url ) . then ( response = > { dispatch ( receiveAdvertizers ( response . data ) ) ; } ) . catch ( err = > dispatch ( receiveAdvertizersError ( err ) ) ) ; } const initialState = { loading : false , loaded : false , error : false , advertizers : null , } export const reducer = ( state = initialState , action ) = > { switch ( action . type ) { case 'REQUEST_ADVERTIZERS' : { return Object . assign ( { } , state , { loading : true , error : false } ) ; } case 'RECEIVE_ADVERTIZERS_ERROR' : { return Object . assign ( { } , state , { error : true , loading : false , loaded : true } ) ; } case 'RECEIVE_ADVERTIZERS' : { return Object . assign ( { } , state , { error : false , loading : false , loaded : true , advertizers : action . payload . data } ) ; } default : return state ; } }

Why look for “better” Redux?

Whenever, we develop a Redux store for our applications, for all load and save methods, we need to build two or three actions to reflect evolutions in our state lifecycle. One action is dispatched at the beginning of each request loading or saving data, one is dispatched at the end, and if the load/save could produce an error, for example, due to a possible problem with an underlying API, we need to dispatch an action to react to the error.

While dispatching actions is simple and straightforward, writing repetitive code fragments is a tedious thankless task.

Rematch

One of the available solutions is the Rematch framework. Rematch is designed to help programmers write code that utilizes Redux best-practices without action types, action creators, switch statements or thunks.

The Rematch version of state management looks like:

export default { state: { advertizers: null }, reducers: { updateState(state, payload) { return { ...state, ...payload }; } }, effects: dispatch => ({ async loadAdvertizers(payload, rootState) { const result = await fetch(`${API_BASE}/advertizers`, { method: 'get', headers: { 'Content-Type': 'application/json; charset=utf-8' } }); if (result.status === 200) { const data = await result.json(); dispatch.advertizers.updateState({ advertizers: data }); return true; } if (result.status === 401) { return false; } } }) }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 export default { state : { advertizers : null } , reducers : { updateState ( state , payload ) { return { . . . state , . . . payload } ; } } , effects : dispatch = > ( { async loadAdvertizers ( payload , rootState ) { const result = await fetch ( ` $ { API_BASE } / advertizers ` , { method : 'get' , headers : { 'Content-Type' : 'application/json; charset=utf-8' } } ) ; if ( result . status === 200 ) { const data = await result . json ( ) ; dispatch . advertizers . updateState ( { advertizers : data } ) ; return true ; } if ( result . status === 401 ) { return false ; } } } ) } ;

The Rematch implementation is clearly more concise than the classic Redux version. Yet, our original Redux version is also slightly different. It keeps several flags – loading, loaded and error, as a part of our state, while rematch state does not have those flags. One of the common uses for the loading flag is to use it with a progress indicator as below:

const { loading } = this.props; return loading ? ( <div>Loading ...</div> ) : ( // main JSX displayed when data is loaded ... ); 1 2 3 4 5 6 7 const { loading } = this . props ; return loading ? ( < div > Loading . . . < / div > ) : ( // main JSX displayed when data is loaded . . . ) ;

In order to implement similar functionality with rematch, we could add the loading flag to the state and invoke two more dispatch calls so our loadAdvertizers() function would look like:

async loadAdvertizers(payload, rootState) { dispatch.advertizers.updateState({ loading: true }); const result = await fetch(`${API_BASE}/advertizers`, { method: 'get', headers: { 'Content-Type': 'application/json; charset=utf-8' } }); if (result.status === 200) { const data = await result.json(); dispatch.advertizers.updateState({ advertizers: data, loading: false }); return true; } if (result.status === 401) { dispatch.advertizers.updateState({ loading: false }); return false; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 async loadAdvertizers ( payload , rootState ) { dispatch . advertizers . updateState ( { loading : true } ) ; const result = await fetch ( ` $ { API_BASE } / advertizers ` , { method : 'get' , headers : { 'Content-Type' : 'application/json; charset=utf-8' } } ) ; if ( result . status === 200 ) { const data = await result . json ( ) ; dispatch . advertizers . updateState ( { advertizers : data , loading : false } ) ; return true ; } if ( result . status === 401 ) { dispatch . advertizers . updateState ( { loading : false } ) ; return false ; } }

This, however, is unnecessary. Instead, we could use the existing Rematch loading plugin.

While we at Diophant never had problems with vanilla Redux, it appears that some developers considered Redux difficult to master, hence instead of functional Redux, some developers were using MobX perceived to be easier to learn. Rematch and similar libraries could help junior programmers get started with functional JavaScript application state management.