Redux = Reduce + Flux
In order to understand Redux, we need to introduce two ideas: the reduce function and the Flux architecture.
Reduce
Functional programming is based around the idea that functions can be passed around as values, that they are first-class objects. Functions that take other functions as arguments are called higher order functions.
Operating on collections is a common use-case for higher order functions.
You should already be familiar with the map
higher order function. map
takes a list and a function of one argument. It then applies the function to each element of that list.
> [1, 2, 3, 4].map((a) => a + 1);
[2, 3, 4, 5]
In the example above, the function (a) => a + 1
will be applied to 1, 2, 3, and then 4, resulting in 2, 3, 4, 5 respectively.
Another commonly used higher order function is filter
. Just like map
, filter
takes a list and a function of one argument. The function here must return either true
or false
and the filter
will return all elements of the list that return true
when evaluated by the function.
> [1, 2, 3, 4].filter((a) => a % 2 == 0);
[2, 4]
The higher order function we’re most interested in is reduce
. This one is a bit more complicated. It feeds a list into an accumulator, with a function to decide what each element does to the accumulator.
> [1, 2, 3, 4].reduce((a, b) => a + b, 0);
10
Think of it as a conveyer belt.
In fact all higher order functions can be implemented as a reduce. Let’s take filter
for example.
const filter = (list, filt) => {
return list.reduce((acc, next) => {
if (filt(next)) {
return acc.concat(next);
} else {
return acc;
}
}, []);
}
Exercise for the reader to figure out what’s going on here.
Flux
Flux is a bit more complicated to explain. I recommend taking a look at the original Flux presentation by Jing Chen to fully understand its motivations.
Flux is intended to solve the flow of data problem in frontend applications.
As we discussed previously, frontend applications depend on asynchronous user actions. This can get very complicated as user actions can feed into views that then feed back into other user actions. This became a big problem with MVC where a single model could be tied to multiple views and vice versa.
Having the caller drive the action is a PITA (see callback hell.)
What if you could use a single direction data flow using messages? A user initiates an action, which creates a message sent to the dispatcher, which modifies the store based on some business logic, which then modifies the view based on some display logic.
Example from the dispatcher docs:
Redux
What you may have noticed is how similar the Flux flow is to the reduce function. Things (action messages) are presented (dispatcher) to a function that then makes a modification to an accumulator (store).
Redux dispenses with the dispatcher in favor of reducers. Reducers are reduce functions that consume an action at a time.
Where Redux comes in is by using reducers to modify the store one message at a time. Reducers are pure functions that can modify the store predictably.
Let’s take the example from the Redux docs.
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
Whenever this reducer gets an INCREMENT message, it will return the current state with 1 added to it. This will then become the new current state. This is just like what happened with our reducer on a collection, except now we’re operating on a stream of events.
So with Redux, our architecture looks similar to the reduce
function, something like this:
When we generate a message, we send it to every reducer for the app. These reducers can choose to ignore the message or use it to modify its state. Thus state modification under Redux becomes message processing.