Reducers
A reducer is a function that gets the current state and an action (an object
with the format { type: 'some type', payload: 'some value' }
and returns a
completely new state.
Reducers make updates predictable – since they are pure by definition –, and, therefore reproduceable.
They have the following signature: Model, Action -> Model
, where Model
is
any value, and Action
is object { type: string, payload: any, * }
. This
means the action object must have at least the type
and payload
keys, but
can have any other key you want.
Example:
const calculatorReducer = (state, { type, payload }) {
switch (type) {
case 'add':
return state + payload
case 'subtract':
return state - payload
case 'multiply':
return state * payload
case 'divide':
return state / payload
default:
return state
}
}
More idiomatic reducers
Many people are put off for the usage of switch
. Although this is an
aesthetic judgement and there's nothing intrinsically wrong with it, there
are more idiomatic ways to write reducers.
First of all a switch statement is the poor cousins of pattern matching and guards. Take a look:
calculatorReducer "add" state payload = state + payload
calculatorReducer "subtract" state payload = state - payload
calculatorReducer "multiply" state payload = state * payload
calculatorReducer "divide" state payload = state / payload
calculatorReducer _ state _ = state
-- or
calculatorReducer type state payload
| eq "add" = state + payload
| eq "subtract" = state + payload
...
where eq = (===) type
This would be the equivalent of our previous example in Haskell. Of course, we
can improve this my using Haskell's features, specially, I would say, proper
types instead of strings for the action types (now you probably have a better
clue why it is called "type" in JS :D), but the good news it that there is an
interesting function in Ramda to get us a little closer to that, namely,
cond
.
Here's the gist of cond
:
const fn = cond([
[equals(0), always('water freezes at 0°C')],
[equals(100), always('water boils at 100°C')],
[T, (temp) => `nothing special happens at ${temp}°C`]
])
fn(0) //=> 'water freezes at 0°C'
fn(50) //=> 'nothing special happens at 50°C'
fn(100) //=> 'water boils at 100°C'
So if the value passed to the function matches 0, it runs the first function
and so on, until getting to final condition T
, which is basically the "true
function", i.e a function that always returns true (like in () => true
), so
it always matches any unmatched value, working pretty much as Haskell's
otherwise
or JS's default
.
So one more idiomatic - in the functional context – way of building a reducer, would be to resort to this helper:
const typeHandlers = cond([
[equals("add"), (type, state, payload) => state + payload],
[equals("subtract"), (type, state, payload) => state - payload],
[equals("multiply"), (type, state, payload) => state * payload],
[equals("divide"), (type, state, payload) => state / payload],
[T, (type, state, payload) => state]
])
const calculatorReducer = (state, { type, payload }) =>
typeHandlers(type, state, payload)
Although this is more composable than a switch, since you can add items to the list dynamically, and more idiomatic functionally speaking, it seems still too verbose and not really idiomatic in a JavaScript context (which I guess it's a hard to define thing to do... probably idiomatic JavaScript is either jQuery or injecting logic as tag's attributes :D).
So by looking at cond
and the specific use of it in reducers, it seems it's
always the case we wanna use equals
in the condition (except for T), and
always a function with the reducer arguments as the 2nd parameter. Therefore,
it is natural to assume we could end up with a configuration like this:
{
add: (state, payload) => state + payload,
subtract: (state, payload) => state - payload,
...
}
We could also remove the action
being passed to the function (something that
cond
would always do) by wrapping it, and have the T condition as a default.
This is what you'll find in the guard
helper. It is a little
syntactic sugar over Ramda's cond
. Here's the full example:
const calculatorReducer = guard({
add: (state, payload) => state + payload,
subtract: (state, payload) => state - payload,
multiply: (state, payload) => state * payload,
divide: (state, payload) => state / payload
})
Mutch more brief.
You will find something very similar in the Redux world in redux-actions's handleActions.
But still, if we look into Ramda's utilities we will find
functions with exactly the same signatures we need here, namely Number → Number
→ Number
(gets two numbers, returns a number). As examples,
add
,
subtract
,
multiply
and
divide
.
Therefore, we can do:
const calculatorReducer = guard({
add: add,
subtract: subtract,
multiply: multiply,
divide: divide
})
And finally, this is the same as:
const calculatorReducer = guard({ add, subtract, multiply, divide })
As a final note, you may be wondering what are the advantages of implementing
this on top of cond
, instead of a more "traditional" (in the JS world) way,
like other libraries do. Apart from the fact it follows the general Act way of
building things it gives you some nice alternatives. For instance, if you may need
a more tricky condition, you can still do that, using arrays instead of an object:
const load = equals('load')
const unload = complement(load)
const loading = set(lensProp('loading'))
const reducer = guard([
[load, loading(true)],
[unload, loading(false)] // runs for all actions except `'load'`
])
And as you can see, using Ramda's lens make it all so much nicer.