#javascript #redux #webdev #testing

Redux and single purpose functions

Preamble

On this post I will share a couple of lessons learned while working on SpiderOak Semaphor, where we use:

Even though this post shows functions in the context of Redux, you can extrapolate the idea to any javascript function. I’ll show tests using Jest, but you could use any other tool you like. Jest tests are pretty readable on its own even if you haven’t seen jest tests before.

You should be able to get some ideas from this post even if you don’t know redux/jest.

TL;DR ?

For more details, keep reading :)

Redux’s reducers

Reducers specify how the application’s state changes in response to actions sent to the store. Remember that actions only describe what happened, but don’t describe how the application’s state changes. (from: https://redux.js.org/basics/reducers)

Reducers are presented (almost always) as large functions with a switch to decide what to do depending on the action received.

Let’s see why this is a problem and how we can use a better approach.

Example and problems

Let’s say we want to store some users on Redux. We’ll need actions to add and remove a user.

This is usually how we see reducers on tutorials:

// e.g. src/actions/users.js
export const USER_ADD = "USER_ADD";
export const USER_REMOVE = "USER_REMOVE";

// e.g. src/reducers/usersById.js
export function usersById(state = {}, action) {
  let newState;
  switch (action.type) {
    case USER_ADD:
      return {
        ...state,
        [action.user.id]: action.user,
      };
    case USER_REMOVE:
      newState = { ...state };
      delete newState[action.id];
      return newState;
    default:
      return state;
  }
}

This approach has several problems:

Note that these problems are really noticeable as the codebase grows, not really for such contrived example.

Adding tests

We will fix our problems by doing some refactor, but before that, we’ll write some tests for our reducer.

Disclaimer: real world code will be more complex, and adding tests will make more sense than on this contrived example.

I recommend you write tests before starting to make changes to your working code, so you can be sure it still works after the changes.

Here’s an example (using jest) of how a test for this use case could look:

// e.g. src/reducers/usersById.test.js
import { userAdd } from 'actions/users';
import { usersById } from 'reducers/usersById';

test('on USER_ADD, with empty state', () => {
  // define your initial state
  const initialState = {};

  // define payload for the action we'll use to test the reducer
  const user = { id: 'user-id-1', username: 'john-doe-01' };

  // define expected state
  const expectedState = {
    [user.id]: user,
  };

  // call our reducer
  const result = usersById(initialState, userAdd(user));

  // check that the result we got is what we need
  expect(result).toEqual(expectedState);

  // check that we don't mutate the state
  expect(result).not.toBe(initialState);
});

Note that to test our reducers we don’t even need Redux, they are just functions.

We don’t (directly) test our action creators since they should be as simple as:

// e.g. src/actions/users.js
export const USER_ADD = "USER_ADD";

export function userAdd(user) {
  return {
    type: USER_ADD,
    user,
  }
}

We test them indirectly by calling them within our reducer’s test, which should be enough.

Single purpose functions

This is a very simple concept, nothing special, just a function with a single purpose instead of multiple ones.

Our reducer is presented as a “monolithic” function with a giant switch. It has several problems, as we listed above.

We’ll split it up into smaller, single-purposed, and more manageable functions. Let’s dig into it.

Refactoring

Now that we have tests for our reducer, we can confidently refactor them or swap implementations with a different one without breaking the rest of our code.

Let’s tackle the problems we listed for our example.

Using the strategy proposed on the redux docs we can do some refactor on our reducers to make them look like this:

// e.g. src/actions/users.js
export const USER_ADD = "USER_ADD";
export const USER_REMOVE = "USER_REMOVE";

// e.g. src/reducers/usersById.js
export function usersById(state = {}, action) {
  switch (action.type) {
    case USER_ADD: return addUser(state, action.user);
    case USER_REMOVE: return removeUser(state, action.id);

    default: return state;
  }
}

function addUser(state, user) {
  return {
    ...state,
    [user.id]: user,
  };
}

function removeUser(state, id) {
  const newState = { ...state };
  delete newState[id];
  return newState;
}

The refactor should be pretty straightforward, just extract the usual reducer logic for each case, into functions.

Let’s contrast this approach with the problems we saw on the earlier example:

Run some code

If you want to take a look at our example with more tests and a couple of extra actions go to this repo.

Or, to play with it live, thanks to CodeSandbox:

Edit test-refactor-redux-reducers

Disclaimer

I wrote this article for the SpiderOak engineering blog and it was published on Jan 18, 2019. https://engineering.spideroak.com/redux-and-single-purpose-functions/

The original post is licensed as: Creative Commons BY-NC-ND