Overview

Developing modern offline apps with ReactJS, Redux and Electron – Part 3 – ReactJS + Redux

No Comments

In the last article we introduced you to the core features and concepts of React. We also talked about the possibility to save data in the component state, pass it to child components and access the data inside a child component by using props. In this article we will introduce Redux, which solves the problem of storing your application state.

 

  1. Introduction
  2. ReactJS
  3. ReactJS + Redux
  4. Electron framework
  5. ES5 vs. ES6 vs. TypeScript
  6. WebPack
  7. Build, test and release process

Once a component needs to share state with another component, that it does not have a parent-child relationship with, things start to get complicated. The following diagram visualizes that problem. On the left hand side, you see a tree of React components. Once a component initiates a state change, this change needs to be propagated to all other components that rely on the changed data.

This is where Redux comes in handy. Redux is a predictable state container for JavaScript apps. The state is kept in one store and components listen to the data in the store that they are interested in.

Flux pattern

Redux implements the Flux pattern that manages the data flow in your application. The view components subscribe to the store and react on changes. Components can dispatch actions that describe what should happen. The Reducers receive these actions and update the store. A detailed explanation of the four parts of the flux pattern in Redux is given in the next sections.

Redux

The Redux state stores the whole application data in one object tree that is accessible from every component of the application. In our example the state contains a small JavaScript object, as you can see in the following code snippet.

const state = {
  isModalOpen: false,
  clipboard: {
    commands[]
  } 
}

The state is immutable and the only way to change it, is to dispatch an action.

Action

Actions are plain JavaScript objects consisting of a mandatory TYPE property to identify the action and optional information. The type should be a string constant that is stored in a separate module to obtain more clarity. There are no naming specifications for the implementation of the object with the additional information. The following example action sets the value of isModalOpen to false.

actionConstants.js
const SET_MODAL_OPEN = ‘SET_MODAL_OPEN’;
modalAction.js
{
  type: SET_MODAL_OPEN,
  payload: false
}

Alternatively you can use an action creator, to create the action. They make the action more flexible and easy to test. In our example we use one action, to set isModalOpen variable to false or true.

function setModalOpen(isModalOpen) {
  return {
    type: SET_MODAL_OPEN,
    payload: isModalOpen
  };
}

The question remains, how you can trigger the action. Answer: Simply pass the action to the dispatch() function.

dispatch(setModalOpen(false));

Alternatively you can use a bound action creator that dispatches the action automatically, when you call the function. Here is an example for that use case:

Bound Action Creator
const openModal = () => dispatch(setIsModalOpen(true));

So far we can dispatch an action that indicates that the state has to change, but still the state did not change. To do that we need a reducer.

Reducer

“Reducers are just pure functions that take the previous state and an action, and return the next state.” [REDUCER]

The reducer contains a switch statement with a case for each action and a default case which returns the actual state. It is important to note that the Redux state is immutable, so you have to create a copy from the state that will be modified. In our projects we use the object spread operator proposal, but you can also use Object.assign(). The following example sets isModalOpen to the value of the action payload and keeps the other state values.

Object spread operatorObject.assign()
function modal(state, action) {
  switch (action.type) {
    case SET_MODAL_OPEN: 
      return {
        ...state,
        isModalOpen: action.payload
      })
      default:
        return state
    }
}
function modal(state, action) {
  switch (action.type) {
    case SET_MODAL_OPEN: 
      return Object.assign({}, state, {
        isModalOpen: action.payload
      })
    default:
      return state
  }
}

The Reducer can either take the previous state if one exists or the optional initial state to define a default on the store properties. In our example we configure that the modal should be closed initially.

const initialState = {
  isModalOpen: false
};

function modal(state = initialState, action) {
  switch (action.type) {
    case SET_MODAL_OPEN: 
      return {
        ...state,
        isModalOpen: action.payload
      })
    default:
      return state
   }
}

The number of reducers can become very large, thus it is recommended to split the reducers into separate files, keep them independent and use combineReducers() to turn all reducing functions into one, which is necessary for the store creation.

Store

We have already talked a lot about the store, but we have not looked at how to create the store. Redux provides a function called createStore() which takes the reducer function and optionally the initial state as an argument. The following code snippets show how to combine multiple reducers, before creating the store.

One reducer
import { createStore } from 'redux';

const initialState = {
  isModalOpen: false,
  clipboard: {
    commands[]
  } 
};

let store = createStore(modalReducer, initialState);
Two combined reducer
import { createStore, combineReducers } from 'redux'; 

const initialState = {
  isModalOpen: false,
  clipboard: {
    commands[]
  } 
};

const reducer = combineReducers({
  clipboardReducer,
  modalReducer
});

let store = createStore(reducer, initialState);

Usage with React

We showed how to create and manipulate the store, but we did not talk about how a component access the store. The component can use store.subscribe() to read objects of the state tree, but we suggest to use the React Redux function connect(), which prevents unnecessary re-renders.

The function connect() expects two functions as arguments, called mapStateToProps and mapDispatchToProps. Decorators are part of ES7 which we cover in blog article 5 on “ES5 vs. ES6 vs. TypeScript”.

With a decorator (ES7)Without a decorator
@connect(mapStateToProps, mapDispatchToProps)

class App extends React.Component {
  render() {
    return (
      <div>
        Count: {this.props.counter}
      </div> 
     );
  }
}


class App extends React.Component {
  render() {
    return (
      <div>
        Count: {this.props.counter}
      </div> 
    );
  }
}

export default connect(
  mapStateToProps, 
  mapDispatchToProps)(App);

mapDispatchToProps defines which actions you want to be able to trigger inside your component. For example we want the Modal to inject a prop called onSetModalOpen, which dispatches the SET_MODAL_OPEN action. If the action creator arguments match the callback property arguments you can use a shorthand notation.

mapDispatchToPropsShorthand notation
const mapDispatchToProps = dispatch => ({
  onSetModalOpen(value) {
    dispatch(setModalOpen(value));
  }
});

connect(mapStateToProps, mapDispatchToProps)(App);
connect(
  mapStateToProps, 
  {onSetModalOpen: setModalOpen}
)(App);



mapStateToProps defines how to convert the state to the props you need inside your component.

const mapStateToProps = state => ({
  isModalOpen: state.modal.isModalOpen,
  clipboard:   state.clipboard    
});

To handle the growing complexity of the store as you write business applications, we recommend to use selectors that are functions that know how to extract a specific piece of data from the store. In our small example selectors do not offer much benefit.

SelectormapStateToProps
const getModal = (state) => {
  return state.modal;
};

const getIsModalOpen = (state) => {{
  return getModal(state).isModalOpen;
};
const mapStateToProps = state => ({
  isModalOpen: getIsModalOpen(state),
  clipboard:   getClipboard(state)
});



Debugging using the Console Logger

Redux provides a predictable and transparent state, that only changes after dispatching an action. To isolate errors in your application state you can use a middleware like redux-logger instead of manually adding console logs to your code.  The following code snippet shows how to configure the default redux logger.

import { applyMiddleware, createStore } from 'redux';
import { logger } from 'redux-logger';
const store = createStore(
  reducer,
  applyMiddleware(logger)
);

When running your React application the redux logger will print the actions to your browser console. By default you see the action name and you can collapse each action to see more details.


In the details view the redux logger shows the previous state of the redux store, then the action with the payload you triggered and after that next state with the new state.

 

Redux logger provides various configuration options. You can specify which entries should be collapsed by default, or which actions should not be logged to the console, just to name a few.

import { applyMiddleware, createStore } from 'redux';
import { logger } from 'redux-logger';
const logger = createLogger({
  collapsed: (getState, action, logEntry) => !logEntry.error,
  predicate: (getState, action) => 
    action  && action.type !== ‘SET_LINES’
});

const store = createStore(
  reducer,
  applyMiddleware(logger)
);

Summary

In this article we showed how useful Redux is to manage the state of applications. The simple flux pattern scales extremely well also for large applications and we did not run into any critical performance issues so far in our projects. In the next article we will introduce Electron and show how to package our React/Redux web app as a cross platform desktop application. Stay tuned 🙂

References

 

Comment

Your email address will not be published. Required fields are marked *