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);
}
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) {
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
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':
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
};
}
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).
Roll the Dice: Random Numbers in Redux was originally published by Dave Ceddia at Dave Ceddia on February 21, 2017.
CodeProject