Blog

Victor is a full stack software engineer who loves travelling and building things. Most recently created Ewolo, a cross-platform workout logger.

Writing an application in React using Redux for state management

This post is a summary of my experience building Ewolo (a workout progress tracker) using React and Redux, with a focus on how Redux fares as a state management library. TLDR; Redux is awesome.

Introduction

The Redux philosophy is that you keep all your data in a single store and operate on it via actions. This advantageous for the following reasons:

  • Increased separation of concerns leads to improved testing.
  • A single source of truth leads to easy modelling of application interactions.
  • Using immutable data structures (which is recommended) leads to less errors.

Since React is technically just a view library and Redux is technically just a state management library, a lot of architectural and design decisions need to be made. There are a lot of options available and the choices that you initially make may not always be the most optimal for your application. For example:

  • How to handle async operations in Redux?
  • To use server-side rendering or to not use server-side rendering?
  • How to organize code?

The application

Over 10 years ago, I had built a Facebook application for logging workouts. At one point, the application even had over 1000 daily active users! Unfortunately, I could not keep up with the platform updates and therefore needed to decommission the application. Since then, I've constantly been on the lookout for a simple cross-platform application which will let me log my workout activity and track my progress.

I used to be pretty old school - I did not bring my phone with me while I was at the gym and tracked everything using pen/paper. However, I still wanted to be able to either prepare my workouts for the week or see a graph of my progress. I kept looking around and it seemed that most of the solutions were apps and the bigger players like exercise.com had really clunky and confusing UX.

This led me to creating Ewolo, a dead simple workout logging platform. Ewolo is designed from the ground up to be mobile friendly and cross-platform. I had previously built a few applications using plain React but I wanted to learn Redux so this was an excellent opportunity to do so. This post is my experience developing the front-end for Ewolo. Note that the front-end code is open sourced and you can follow along while reading the article here.

Since creating Ewolo, I have also started using it to log my workouts in the gym and found it to be much faster than the pen/paper approach, check it out! :)
Starting out

Creating a React application today has been vastly simplified with the introduction of the create-react-app. Simply create a new React application, npm install and start coding!

The React documentation also has a really nice article on getting started aptly titled Thinking in React. The idea behind creating a React application is to think in terms of components - create a sketch of the UI on paper and then break it down into reusable components where necessary.

In general, React's API is not very large and the documentation is also fairly decent. I personally found the list of events to be quite bare but an internet search will usually point you in the right direction. For a React events cheatsheet, look here.

One of the first decisions that need to be made when introducing Redux is code organization. The way I approached this was to keep the state management separate from the React UI as much as possible. The idea behind this is that I can swap out React for a different view library and not have to change the state management at all. Thus, the folder structure under src now looks like the following:

  • modules: Redux modules - signup, login, log-workout, etc. These usually end up as a 1-to-1 mapping to UI components
  • components: React components - a single top-level component generally references one state management module but can potentially references other modules as well. For e.g. a login component might reference the login and global modules to handle login actions and show app notifications.
  • redux: Additional Redux middleware and helper methods such as createStore.js
  • services: To the extent possible, business logic is encapsulated here. These should ideally be simple functions with no side-effects.
  • common: Common utility code, for e.g. get a properly formatted date, browser window width, application constants, etc.

Ewolo code organization - note that the auth components reference the login and signup modules.

The goal of the above organization is to keep the Redux modules as simple as possible with all business logic residing in services. This allows for some really good test coverage as the concepts are smaller to test. In most cases, the application will be interacting with an API and thus the Redux store should ideally only normalize and store this data. The business services then take this raw data and prepare it for easy consumption by the UI.

Redux

The Redux philosophy is that you keep all your data in a single store and operate on it via actions. A picture is worth a 1000 words:

Redux data flow - Source unknown.

Note that in the above diagram the dispatcher and the state are provided by Redux. The store is basically a glorified json object that contains all your data and is only setup once at the start of the application. The green component is the UI and you only really need to concern yourself with the Actions and the Reducer concepts.

Compared to other frameworks such as Angular 1, I found myself writing a lot of code. However, I found out very quickly that this verbosity was actually the magic sauce that gives Redux it's appeal of predictablity and testability. Let's run through all the concepts again taking the example of a simple login page that takes the email, password and logs a user in if successful.

Action types

An action type is simply a constant that represents an action name. It can be thought of as a java enum and simply exists to identify various actions. In my opinion, it is a nice-to-have feature which can be used to better refactor code. The ewolo front-end uses a 1-to-1 mapping between action names and the action type constants so they are not always defined for every module. See loginActionConstants.js:

  export const c = Object.freeze({
    LOGIN_SET_DATA: 'LOGIN-SET-DATA', // action used by the form to maintain the user input
    LOGIN_SET_AFTER_SUCCESS: 'LOGIN-SET-AFTER-SUCCESS' // action used by the application to set a redirect after success
  });
Action creators

Action creators are essentially functions that return an action object. The action objects are just simple JavaScript objects with a guarantee that there exists a type property which is usually a string and corresponds to the action type. The ewolo front-end does not do much work here except for simply setting the various data properties in the action for the reducer to process. See loginActions.js:

  const loginActions = {
    loginSetData: (email, password, text) => {
      return {
        type: c.LOGIN_SET_DATA,
        email: email,
        password: password,
        text: text // 
      };
    },
    loginSetAfterSuccess: (redirect = '/') => {
      return {
        type: c.LOGIN_SET_AFTER_SUCCESS,
        redirect: redirect
      };
    }
  }

  export default loginActions;

As mentioned earlier, all business logic is encapsulated in external services which are primarily used within the reducers. This is mainly to keep testing coherent.

Reducers

The reducer in react is basically a function that takes as parameters the current state and an action. It then applies the action to the state and returns a brand new state. This is the heart of redux and there are some points to note here:

  • States should be immutable, i.e. always return a brand new state. Redux comes with helpers that allow splitting of the state into separate concerns so that you don't end up writing one massive function.
  • No asynchronous operations here. There is middleware written specifically for this which we will look at in the following sections.
  • If the action type does not match anything, return the original state. This allows the action to pass through all reducers and be handled where appropriate.
See loginReducer.js:

  export const initialState = {
    email: '',
    emailFormHint: '',
    password: '',
    passwordFormHint: '',
    text: null,
    afterSuccess: {
      redirect: '/' // by default redirect home after login
    }
  };
  
  const loginReducer = (state = initialState, action) => {
    switch (action.type) {
      case c.LOGIN_SET_DATA:
        {
          const { email, password, text } = action;
  
          return {
            ...state,
            email: email,
            password: password,
            text: text,
            emailFormHint: ewoloUtil.validateEmail(email),
            passwordFormHint: ewoloUtil.validatePassword(password)
          };
        }
      case c.LOGIN_SET_AFTER_SUCCESS:
        {
          const redirect = action.redirect;
  
          return {
            ...state,
            afterSuccess: {
              redirect: redirect // can potentially set an action here as well
            }
          };
        }
      default:
        return state;
    }
  };
  
  export default loginReducer;  

Note that the reducer here is not only setting the state properties but also determining if there is a form hint to be set via helper methods. Writing tests for reducers is dead simple since they are basically normal JavaScript functions, loginReducer.test.js:

  import { expect } from 'chai';
  
  import loginReducer, { initialState } from './loginReducer';
  import actions from './loginActions';
  import c from './loginActionConstants';
  
  describe('loginReducer', () => {
    it('should reduce undefined state to initial state', () => {
      // when
      const newState = loginReducer(undefined, { type: '' });
  
      // then
      expect(newState)
        .to
        .deep
        .equal(initialState);
    });
  
    describe(c.LOGIN_SET_DATA, () => {
      it('should set data', () => {
        // when
        const newState = loginReducer(undefined, actions.loginSetData('vic@smalldata.tech', 'sdksdfsdfsdf', 'xxx'));
  
        // then
        const expectedState = {
          ...initialState,
          email: 'vic@smalldata.tech',
          password: 'sdksdfsdfsdf',
          emailFormHint: '',
          passwordFormHint: '',
          text: 'xxx'
        };
  
        expect(newState)
          .to
          .deep
          .equal(expectedState);
      });
    });
  });  

Testability was a very important factor in my deciding to use Redux and being able to do Test Driven Development was crucial to adding features quickly and refactoring the product.

Store

The store in Redux is the wrapper around the entire state object and provides the following:

  • Allows access to state via getState(). Note that the state is read-only and would need to be updated via an action passed to the reducer.
  • Allows state to be updated via dispatch(action).
  • Registers listeners via subscribe(listener). The idea here is that the view layer would listen to changes on the state and update itself accordingly.
  • Handles unregistering of listeners via the function returned by subscribe(listener).
Creating the store:

  import { createStore } from 'redux'
  import appReducer from './appReducer';
  const store = createStore(appReducer);
  ...

Note that Redux provides helper methods to integrate the store to React component so the subscription and unsubscription is handled auto-magically. Read the section on connecting with container components here.

Middleware

We now come to the most interesting part of developing our application - interacting with an API. Since API calls are asynchronous, we need a simple way to be able to start an async request and handle the response. So far, the data flow in Redux has been synchronous - click a button in the UI, create an action, update the state and have the UI update.

While there are many strategies for dealing with asynchronous operations, the simplest and one of the most popular solutions is redux-thunk. This is a redux middleware implementation that allows writing async action creators. Instead of returning the action that is to be dispatched, these action creators instead return a function that provides access to the dispatch method. This access can then be used to start an async API request and then dispatch a success or a failure action depending on the result. To use redux-thunk, simply configure the store via the applyMiddleware helper:

  import thunkMiddleware from 'redux-thunk';
  import { createStore } from 'redux'
  import appReducer from './appReducer';

  const store = createStore(appReducer, applyMiddleware(thunkMiddleware));
  ...

The Redux data flow is now modified as follows:

Redux data flow with side effects - Source unknown.

loginThunk is an async action creator defined in loginActions. Note that the redux-thunk middleware also provides a getState method which can be used to retrieve data from the current state. Of course, since this is again just a normal function, data may also be passed via parameters :

  ...
  loginThunk: () => {
    return (dispatch, getState) => {
      const login = {
        ...getState().login
      };
      
      const afterSuccess = {
        ...login.afterSuccess // what to do after logging in
      };

      dispatch(globalActions.taskStart());

      const promise = ewoloUtil.getApiRequest({
        route: '/authenticate',
        method: 'POST',
        body: { email: login.email, password: login.password }
      });

      return promise
        .then(ewoloUtil.getApiResponse)
        .then(body => {

          dispatch(userDataActions.processUserAuthSuccess(body.token));
          if (afterSuccess.redirect) {
            dispatch(push(afterSuccess.redirect));
          }

        })
        .catch(error => {
          dispatch(globalActions.userNotificationAdd('ERROR', 'Invalid username / password'));
        })
        .then(() => {
          dispatch(globalActions.taskEnd());
        });
    };
  }
  ...

Handling side-effects in Redux is a subject with lots of literature as there are many different ways of doing things. Most solutions involve modifying the action creation operation to dispatch the correct operations but there also exists middleware which introduces async operations into the reducer. For the most part, Ewolo uses redux-thunk for API operations. There are other side-effects which are handled separately and they will be covered in more detail in a separate blog post.

You may have also noticed that the loginThunk action creator dispatches a global taskStart action which is responsible for showing a spinner and the taskEnd operation stops it. This can of course be more fine grained with LOGIN_REQUEST_START and LOGIN_REQUEST_END actions. The best part of this setup is again the ease of testing, loginActions.test.js:

  import nock from 'nock';
  import { expect } from 'chai';
  import configureMockStore from 'redux-mock-store';
  import thunkMiddleware from 'redux-thunk';

  const store = configureMockStore([thunkMiddleware]);

  describe('loginActions', () => {
    afterEach(() => {
      nock.cleanAll();
    });
  
    it('should successfully login and redirect', () => {
      nock(ewoloConstants.api.url)
        .post('/authenticate')
        .reply(200, { token: ewoloTestUtil.authToken });
  
      const expectedActions = [
        { type: 'TASK-START' },
        {
          type: 'USER-DATA-AUTH-SUCCESS',
          authToken: ewoloTestUtil.authToken,
          id: ewoloTestUtil.authTokenUserId
        },
        {
          type: '@@router/CALL_HISTORY_METHOD',
          payload: { method: 'push', args: ['/xxx'] }
        },
        { type: 'TASK-END' }
      ];
  
      const store = mockStore({
        login: {
          email: 'vic@smalldata.tech',
          password: 'xxx',
          afterSuccess: {
            redirect: '/xxx'
          }
        }
      })
  
      return store.dispatch(loginActions.loginThunk())
        .then(() => { // return of async actions
          const actions = store.getActions();
          const authToken = ewoloUtil.getObject(ewoloConstants.storage.authTokenKey);
          expect(authToken).to.equal(ewoloTestUtil.authToken);
          expect(actions).to.deep.equal(expectedActions);
        });
    });
  });

The push method here is a helper method from the react-router-redux package and allows us to manipulate the url. More on this in a few sections.

React

Since Redux was initially built with React in mind, it is very straight-forward to hook the two together. We won't get into the details but some special considerations that were needed for Ewolo are detailed below.

Components

The recommended approach to building React components is to keep them as dumb and as resuable as possible. This means that the components should get everything they need to passed to them. In order to interact with the world outside them, they can also be passed the respective functions that can be invoked to pass data out. Keeping things dumb leads to some really well encapsulated code and faster design iterations. See here for an example on refactoring components to move common state up.

Forms

Forms are the heart of pretty much any web application and there exist a wide range of libraries with one of the more poular ones being react-redux-form. I chose to stick with basic React for the forms however, and fire off actions on each input event. This seemed counter intuitive initially but it turned out to be a very powerful approach where the state of the form was always known and validation became a breeze:

  import React, {Component} from 'react';
  ...
  import EwoloFormHint from '../generic/EwoloFormHint';
  import loginActions from '../../modules/login/loginActions';
  
  const mapStateToProps = (state) => {
    return {login: state.login};
  };
  
  const mapDispatchToProps = {
    doLoginSetData: loginActions.loginSetData,
    ...
  };
  
  class Login extends Component {
    ...
    handleEmailChange = (event) => {
      this
        .props
        .doLoginSetData(event.target.value, this.props.login.password, this.props.login.text);
    };
  
    handlePasswordChange = (event) => {
      this
        .props
        .doLoginSetData(this.props.login.email, event.target.value, this.props.login.text);
    };
  
    render() {
  
      return (
        <div>
          ...
          <form className="form-horizontal">
            ...
            <input
              className="form-input"
              type="email"
              placeholder="Email"
              value={this.props.login.email}
              onChange={this.handleEmailChange}/>
            
            <EwoloFormHint formHint={this.props.login.emailFormHint} />

            <input
              className="form-input"
              type="password"
              placeholder="Password"
              value={this.props.login.password}
              onChange={this.handlePasswordChange}/>
            
            <EwoloFormHint formHint={this.props.login.passwordFormHint} />

            ...
          </form>
          ...
        </div>
      );
    }
  };
  
  export default connect(mapStateToProps, mapDispatchToProps)(Login);    

The above is a reduced version of the Login.js component.

Routing

Integrating Ewolo with the most popular routing library, react-router gave me an insane amount of grief. The latest major release is 4.0 and is not API compatible with previous releases. Most online help refers to older versions and moreover, the docs themselves are very thin on details, especially with Redux integration. To be fair, the philosophy behind react-router is sound and one is free to use other routing libraries as well. There is a react-router-redux integration package that led me to stick with react-router, however.

The latest react-router-redux which is compatible with react-router 4.0 is not yet released and only available as an alpha version. Fortunately, I was able to use this package and use the push action method to be able to redirect from an action creator thunk:

  import { push } from 'react-router-redux';
  ...
  
  loginThunk: () => {
    return (dispatch, getState) => { 
      ...
      return promise
        .then(ewoloUtil.getApiResponse)
        .then(body => {

          dispatch(userDataActions.processUserAuthSuccess(body.token));
          if (afterSuccess.redirect) {
            dispatch(push(afterSuccess.redirect));
          }

        })
        ...
    };
  }
  ...

Again, to be fair, it would also have been possible to let react-router manage route manipulation and let the components themselves redirect to wherever made sense. However, I wanted to keep my components simple and not have too many conditions on render so I went with the above approach. This also helped me in writing middleware which could check for unauthorized access to routes available only to logged in users (more on this in a separate blog post).

Server-side rendering

Server-side rendering refers to running react on the server and having the server spit out the compiled html which the browser renders right away. This quora article is a really good reference for tradeoffs between client-side and server-side rendering. I decided to stick with client-side rendering with the addition of react-snapshot to generate SEO friendly pages for mostly static content:

  ...
  "scripts": {
    ...
    "build": "react-scripts build && react-snapshot",
    ...
  },
  "reactSnapshot": {
    "include": [],
    "exclude": [
      "/log-workout",
      "/login",
      "/signup",
      "/change-log"
    ],
    "snapshotDelay": 100
  },
  ...

The above snippet is from Ewolo's package.json.

Summary

Prior to this, I had used plain React to build a few small and medium sized front-end applications. I was impressed with React but the lack of a simple state management solution meant that I had to roll my own where necessary. With Redux in the picture, state management is a breeze and the fact that states are immutable means that I am able to reason about the flow of data in my application on a very high level. This helped me be extremely productive and also reduced the scope of potential mistakes.

A special mention goes out to the Redux DevTools chrome extension that you can use to track what is happening in your application. In fact, with the addition of Redux, I never found the need to open the React DevTools extension again!

I hope that you enjoyed reading this article and found it useful. The source for Ewolo is available on Github and I am happy to receive any feedback.

HackerNews submission / discussion

Back to the article list.