Open Source React.js Software

Make Redux Reducers Readable using the Factory Pattern

If you’ve never used Redux before, a common issue that most developers will run into is the readability of the reducers that are built to maintain state. How do we know that this is a problem? Redux itself has a page on refactoring reducers to be smaller and more readable.

Here at Merlion Solutions, we are aware that code readability is one of the most important pieces of software development. If you neglect to think about future developers who may step into the code base, you could be met with a very upsetting learning curve. Even the same developers who wrote the code could step away for long enough to forget the architecture. How often do you re-enter your own code a year down the road and feel like there is no possible way you could have been the one who wrote it? It’s a common scenario. We might as well plan for new eyes to enter the code base and expedite the learning curve for them. One way we do that is by using common file structure patterns to ensure quick navigation of code.

Making reading service files easier.

Now imagine the below code block was a bit of legacy node.js code that managed to get way too large. You could have a file with hundreds of lines and over 25 functions in the service file. How would you possibly navigate this if you used the syntax below? Much of the time developers end up reading or navigating through code that is irrelevant to the task they are doing. Collapsing all the functions, just to see all the function names to try and figure out which is the right function to use. The patterns below are common, seen often in node code bases. We’ve even written a few like that ourselves.

exports.addToDO = function () {
    return {};
};

exports.toggleToDO = function () {
    return {};
};

exports.editToDo = function () {
    return {};
};

or even

module.exports = {
    addToDO: function () {
        // imagine if this took 40 lines of code
        return {};
    },
    editToDo: function () {
        // this almost may take 15 lines of code
        return {};
    },
    toggleToDO: function () {
        // 3 functions in and you might be taking
        // up your entire monitors screen space
        return {};
    },
};

Now, if you imaged what the above code would look like if the file was very large, you can see the problem of quick navigation and readability.

Here is an example of how we like to structure service files in our code. We did not use much ES6 to make the code more readable for developers who have not yet delved into enhanced object literals. Using function hoisting, we can use the functions in an object at the top of the file. Essentially generating a table of contents for ourselves.

// Simple object that contains all the exported function in the service.
// This is readable, and allows IDE shortcuts for developers to quickly read
// the name of a function and navigate directly to it. 
const service = {
    addToDo: addToDo,
    editToDo: editToDo,
    toggleToDo: toggleToDo,
};

function addToDo() {
    return {};
}

function toggleToDo() {
    return {};
}

function editToDo() {
    return {};
}

module.exports = service;

Nice, verbose naming conventions become important because I can read the service object at the top like it’s a table of contents. Then, when I find the first function that I need to look at, I simply alt-click on the object’s property in webstorm and I’m navigated to the contents of the function. Using simple organizational patterns enable us to quickly go into a service file and see what functions are available to us and what they do. We’ve also noticed this isolation benefits developers, as they are less likely to touch code that is unrelated to the task they are doing, keeping task scope in check.

Since we had such success with the above pattern, we started to think how could we use a similar technique in other areas. Enter redux.

Readable Redux Reducers.

If you’re still with us, we’re going to show you a cool pattern we’ve implemented in our React.js code successfully.

Below is an example of how to create a reducer, taken from the official Redux website documentation.

const initialState = {
  visibilityFilter: 'SHOW_ALL',
  todos: []
}

function appReducer(state = initialState, action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER': {
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    }
    case 'ADD_TODO': {
      return Object.assign({}, state, {
        todos: state.todos.concat({
          id: action.id,
          text: action.text,
          completed: false
        })
      })
    }
    case 'TOGGLE_TODO': {
      return Object.assign({}, state, {
        todos: state.todos.map(todo => {
          if (todo.id !== action.id) {
            return todo
          }

          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        })
      })
    }
    case 'EDIT_TODO': {
      return Object.assign({}, state, {
        todos: state.todos.map(todo => {
          if (todo.id !== action.id) {
            return todo
          }

          return Object.assign({}, todo, {
            text: action.text
          })
        })
      })
    }
    default:
      return state
  }
}

You’ll notice the example is easy to read at first glance, but what if you add 3 more functions of the same size? It can get out of hand pretty quickly. What we did at Merlion is made a utility function to create reducers for us, the utility function enabled us to use a similar pattern to the one we use in our service files.

The below utility function will make creating reducers more verbose and easy to read. We’re using the factory pattern to make our reducers for us.

// createReducer.js
export const createReducer = (initialState, spec) => {
    return (state = initialState, action) => {
        //  for every key in spec,
        for (let key in spec) {
            // if match return call value
            if (action.type === key) {
                return spec[key](state, action);
            }
        }
        return state;
    };
};

Now, we must use the utility function to make reducers. It allows us to not have to worry about the switch statements, while also giving us an easier to read and navigate reducer file.

import createReducer from 'createReducer.js';

const initialState = {
  visibilityFilter: 'SHOW_ALL',
  todos: []
}

createReducer(initialState, {
    SET_VISIBILITY_FILTER: SET_VISIBILITY_FILTER,
    ADD_TODO: ADD_TODO,
    TOGGLE_TODO: TOGGLE_TODO,
    EDIT_TODO: EDIT_TODO
});

function ADD_TODO(state, action) {
    return Object.assign({}, state, {
        todos: state.todos.concat({
            id: action.id,
            text: action.text,
            completed: false
        })
    });
}

function SET_VISIBILITY_FILTER(state, action) {
    return Object.assign({}, state, {
        visibilityFilter: action.filter
    });
}

function TOGGLE_TODO(state, action){
    return Object.assign({}, state, {
        todos: state.todos.map(todo => {
            if (todo.id !== action.id) {
                return todo
            }

            return Object.assign({}, todo, {
                completed: !todo.completed
            })
        })
    })
}

function EDIT_TODO(state, action){
    return Object.assign({}, state, {
        todos: state.todos.map(todo => {
            if (todo.id !== action.id) {
                return todo
            }

            return Object.assign({}, todo, {
                text: action.text
            })
        })
    })
}

Hopefully the above example is both easier to read and navigate for you. We’ve had success implementing the pattern, it’s very similar to the service file pattern we use.

We will be publishing the utility function to an open source NPM package very soon, if you have any suggestions on the name feel free to shoot us a suggestion. We will update this post with the npm location once it’s available.


Merlion Solutions is a full service Tampa, Florida based software development consulting firm. We tackle projects small and large, with industry experience in Real Estate, Finance, HR, Sales, Government, IoT, Law and more. If you have any questions about the post or want to get in contact about a potential project, feel free to reach out at any time using our live chat or contact form.