reducer, n. – A word Redux made up to confuse you.
In order to work with Redux, you need to know a few things. One of those things is what a reducer is and what it does. It might seem a bit scary and foreign, but after this short article I think you’ll come to agree that it is, as the saying goes, “just a function.”
First off, where does the name “reducer” come from? Redux didn’t actually make it up (I was kidding about that). It might not seem too foreign if you’re familiar with functional programming and JavaScript’s Array.reduce
function. And if you know Array.reduce
, you know that it takes a function (one might call it a “reducer” function) that has the signature (accumulatedValue, nextItem) => nextAccumulatedValue
.
Array.reduce is like a sister to Redux
If you aren’t yet familar with Array.reduce
, here’s what’s up:
JavaScript’s Array has a built-in function called reduce
.
(Technically I should be writing it as Array.prototype.reduce
, because it is a function on array instances, not on the capital-A Array
constructor.)
It takes a function as an argument, and it calls your provided function once for each element of the array, similar to how Array.map
works (or a for
loop, for that matter). Your function gets called with 2 arguments: the last iteration’s result, and the current array element. This will make more sense with an example:
var letters = ['r', 'e', 'd', 'u', 'x'];
// `reduce` takes 2 arguments:
// - a function to do the reducing (you might say, a "reducer")
// - an initial value for accumulatedResult
var word = letters.reduce(
function(accumulatedResult, arrayItem) {
return accumulatedResult + arrayItem;
},
''); // <-- notice this empty string argument: it's the initial value
console.log(word) // => "redux"
In this example, the reducer will get called 5 times (because there are 5 elements in the array). The calls go like this:
- first called with
('', 'r')
=> returns'r'
- the empty string
''
comes from the 2nd argument toreduce
, and the'r'
is the first element of the array
- the empty string
- then
('r', 'e')
=> returns're'
- the ‘r’ comes from the previous return value, and ‘e’ is the next element of the array
- then
('re', 'd')
=> returns'red'
- the ‘re’ is the previous return value, and ‘d’ is the third array element
- then
('red', 'u')
=> returns'redu'
- by now you are sensing a pattern
- then
('redu', 'x')
=> returns'redux'
- the pattern is all too clear now
The last return value, 'redux'
, is returned as the final result and stored in the word
variable.
Redux Reducers
Now that you know how Array.reduce
works, I can tell you that Redux is basically a fancy Array.reduce function (ok ok that’s a huge oversimplification, but bear with me).
A Redux reducer function has this signature:
(state, action) => newState
As in: it takes the current state
, and an action
, and returns the newState
. Looks a lot like the signature of an Array.reduce reducer, eh? Remember:
(accumulatedValue, nextItem) => nextAccumulatedValue
Plainly speaking, a Redux reducer gets to decide how each action affects the state. Let’s look at an example one:
function wordReducer(state = '', action) {
switch(action.type) {
case 'ADD_LETTER':
return state + action.letter;
case 'RESET':
return '';
default:
return state;
}
}
Quick quiz: is there any Redux-specific code in here? Anything that depends on the Redux library to work? Go ahead, think it over, I’ll wait.
…
…
…
Answer: Nope! This is a plain old function. Sure, it takes (state, action)
arguments and returns a new state. And it expects action
to look something like {type: 'ADD_LETTER', letter: 'r'}
. But none of that is particularly bound to Redux.
How it Works
But anyway, what does it actually do? Let’s try calling it with a few things and see what it returns.
let state = '';
console.log(wordReducer(state, {type: 'ADD_LETTER', letter: 'y'}));
// => y
console.log(wordReducer(state, {type: 'ADD_LETTER', letter: 'y'}));
// => y
console.log(wordReducer(state, {type: 'ADD_LETTER', letter: 'y'}));
// => y
First: notice that wordReducer
does not remember anything. It holds no state within.
let state = '';
console.log(wordReducer(state, {type: 'ADD_LETTER', letter: 'a'}));
// => a
console.log(wordReducer(state, {type: 'ADD_LETTER', letter: 'b'}));
// => b
console.log(state)
// => ''
Next: notice that wordReducer
does not change the state. It merely returns a new one. It treats state as immutable. This is important because, by updating state in an immutable way, Redux is able to tell which pieces of state changed, and optimize how your app is re-rendered.
One more thing:
console.log(wordReducer(undefined, {type: 'UNHANDLED'}));
// => ''
console.log(wordReducer('existing state', {type: 'UNHANDLED'}));
// => 'existing state'
Notice that the reducer has an initial state (when given undefined
, it returns an empty string anyway), and that it has a default
case that handles any actions it doesn’t understand (it returns the existing state, unchanged, when it sees such an action).
Pilot Needed
I can tell you don’t think this is very useful. What good is a function that doesn’t remember anything, and doesn’t change anything?
I’ll tell you: This function is nice because it is predictable. If you call it with the same arguments, you get the same outputs, every single time. It doesn’t matter what else has changed in your app – this function will always act the same way.
It is easy to figure out what it does by reading its code (and easy to debug!) because it’s all self-contained.
Now, the downside with a function like this is that it needs a driver of sorts. Something needs to hold on to the intermediate state, otherwise the app won’t really do much of anything.
The driver, in this case, is Redux. Specifically, the Redux store. It does something like this:
let state = '';
state = wordReducer(state, {type: 'ADD_LETTER', letter: 'a'}));
state = wordReducer(state, {type: 'ADD_LETTER', letter: 'b'}));
state = wordReducer(state, {type: 'ADD_LETTER', letter: 'c'}));
The store maintains an internal state
variable. When an action is dispatched, the store calls the reducer, and replaces its internal state
with whatever the reducer returned. Every time the store calls the reducer, it passes in the last-known state.
Around and around it goes: Redux sitting there waiting for an action, handling that action, updating the state, re-rendering your app, on and on forever.
So that’s it! That’s how Redux reducers work, in a nutshell. Not too bad?