Roll the Dice: Random Numbers in Redux

By Dave Ceddia

How would I model calling something like Math.random() in Redux’s world?

One of the tenets of Redux is that reducer functions must be pure. What about when you want to do something impure, like generate a random number, or get the current date?

Recap: What’s a Pure Function?

A pure function is one that follows these rules:

  • No side effects – it can’t change anything outside the function’s scope (this also means it can’t modify its arguments)
  • Same output for same input – calling it with a given set of inputs must produce the same return value, every time (this means no saved state between calls)

Here’s an impure function:

function addItem(items, item) {
  items.push(item);
}

// Used like:
let items = [1, 2];
addItem(items, 3);

It’s impure because it modifies one of its arguments.

Here’s another impure function:

function makePerson(firstName, lastName) {
  // Make an age between 1 and 99
  const age = Math.floor(Math.random() * 99) + 1;

  return {
    name: firstName + " " + lastName,
    age: age
  };
}

This one is impure because it’ll (probably) return a different result when given the same inputs. Call it 3 times like makePerson('Joe', 'Smith') and it will return people with 3 different ages.

Impure Values in Redux

DiceDice by Ella’s Dad. CC BY 2.0.

Now let’s say you need to do something impure, like simulate the roll of two dice, and put the result in the Redux store.

We already know that reducers must be pure – so we can’t call Math.random() in the reducer. Anything impure must come in through an argument. Here is our reducer:

const initialState = {
  die1: null,
  die2: null
};

function diceReducer(state = initialState, action) {
  switch(action.type) {
    case 'RESET_DICE':
      return initialState;

    case 'ROLL_DICE':
      //
      // then a miracle occurs
      //
      return state;

    default:
      return state;
  }
}

The only argument we can affect is action, which we can do by dispatching an action.

So that’s what we’ll do: put the random number into an action.

Option 1: Inside Action Creator

Here’s a straightforward way to do this: generate the random number in an action creator.

function rollDice() {
  return {
    type: 'ROLL_DICE',
    die1: randomRoll(),
    die2: randomRoll()
  }
}

function randomRoll(sides = 6) {
  return Math.floor(Math.random() * sides) + 1;
}

Then dispatch it as usual, with dispatch(rollDice()).

Pros: It’s simple.

Cons: It’s impure, so it’s harder to test. What’re you gonna do, expect(rollDice().die1).toBeCloseTo(3)? That test will fail pretty often.

Option 2: Pass to Action Creator

Here’s a slightly more complicated way: pass in the random numbers as arguments to the action creator.

function rollDice(die1, die2) {
  return {
    type: 'ROLL_DICE',
    die1,
    die2
  };
}

// Then elsewhere in component code...
dispatch(rollDice(randomRoll(), randomRoll()));

function randomRoll(sides = 6) {
  return Math.floor(Math.random() * sides) + 1;
}

Pros: The action creator is pure, and easy to test. expect(rollDice(1, 2).die1).toEqual(1).

Cons: Anything that calls this action creator must know how to generate the random numbers. The logic isn’t encapsulated in the action creator (but it’s still pretty well encapsulated in the randomRoll function).

Back to the Reducer…

Whichever option you choose, the reducer is the same. It returns a new state based on the die values in the action.

const initialState = {
  die1: null,
  die2: null
};

function diceReducer(state = initialState, action) {
  switch(action.type) {
    case 'RESET_DICE':
      return initialState;

    case 'ROLL_DICE':
      return {
        die1: action.die1,
        die2: action.die2,
      };

    default:
      return state;
  }
}

Wrap Up

There’s not too much else to say about impure values in reducers. To recap:

  • Reducers must be pure! Don’t call Math.random() or new Date().getTime() or Date.now() or any other such thing inside a reducer.

  • Perform impure operations in action creators (easy to write, hard to test) or pass the values into the action creators (easy to test, harder to write).