A reducer is a function that takes a collection and for each item in the collection, returns a new state. Most commonly we can use reducers to transform an old state of something to a new state of something. That could be an array to integer, array to array, array of objects representing application state to a new array of objects with the updated application state, anything really.
In most implementations the reduce function relies on 3 key components being available. Firstly is the collection to be reduced, secondly is the reducer function to run for each item in the collection and thirdly is the initial value of the reducer. As an example, in vanilla JavaScript we could do the following:
constnumbersToAdd=[1,2,3];functionadditionReducer(previous,current){returnprevious+current;}constresult=numbersToAdd.reduce(additionReducer,0);console.log(result);// 6
Wereduce
our collection passing in a reducer function which receives aprevious
andcurrent
value and adds the two together and finally we have the initial value of0
. What this will do is run the reducer for each iteration of the collection and use the initial value as the initial value ofprevious
and when we return the result of addingprevious
andcurrent
, that value will then become the value ofprevious
on the next iteration until there are no more items in the collection to iterate and thus, the result is returned.
Tests
describe('reduce',()=>{it('should apply the addition reducer correctly',()=>{constcollection=[1,2,3];constreducerFn=(previous,current)=>previous+current;constactual=reduce(collection,reducerFn,0);constresult=6;expect(actual).toStrictEqual(result);});it('should return a new array of multiplied values correctly',()=>{constcollection=[1,2,3];constreducerFn=(previous,current)=>{previous.push(current*2);returnprevious;};constactual=reduce(collection,reducerFn,[]);constresult=[2,4,6];expect(actual).toStrictEqual(result);});it('should reduce a collection of objects and reshape them via the reducer',()=>{constpokemon=[{name:"charmander",type:"fire"},{name:"squirtle",type:"water"},{name:"bulbasaur",type:"grass"}];functionpokemonReducer(output,current){output[current.name]={type:current.type};returnoutput;}constactual=reduce(pokemon,pokemonReducer,{});constresult={charmander:{type:'fire'},squirtle:{type:'water'},bulbasaur:{type:'grass'}};expect(actual).toStrictEqual(result);});});
Here we can see 3reduce
tests which work on similar data but produce values of differing types. That is to say that we have a simple addition reducer just as with the example given in the introduction of this article but also a more complex multiplication reducer which basicallyacts like amap
function would since it generates a new array of multiplied values. Lastly we see a far more complex reducer which takes a collection of objects and returns a new state representation of each object as a new collection.
Implementation
The native JavaScript implementation ofreduce
has the following signature:
arr.reduce(functioncallback(accumulator,currentValue[,index[,array]]){// perform actions and return the next state}[,initialValue]);
We will aim to reproduce this behaviour with the following implementation:
/** * @function reduce * @description A function to a collections values into any other type * @param {Array} collection - The collection to reduce * @param {Function} reducerFn - The reducer function to be applied on the last and current value * @param {*} initialValue - The initial value to apply the reducer to * @returns {*} The reduced value, this will be the same type as the initialValue parameter */functionreduce(collection,reducerFn,initialValue){letoutput=initialValue;constclone=[...collection];for(letindex=0;index<clone.length;index++){output=reducerFn(output,clone[index],index,clone);}returnoutput;}
TheinitialValue
will be the defaultoutput
of thereduce
function if no items in the collection exist. If items exist in the collection then for each one we will reassignoutput
to the value of thereducerFn
function. ThereducerFn
function takes the same parameters as the native JavaScript implementation since that is our goal to reproduce. These parameters are theaccumulator
,currentValue
,index
,array
in the native implementation but in our case they areoutput
,clone[index]
,index
andclone
.
Note: If you haven't read the previous articles on Array method algorithms in this series, we are cloning the array to avoid mutations on the initial collection that is provided so that that remains intact.
Finally, once ourreducerFn
function commits actions against each element and generates a finaloutput
value, we exit the loop and return theoutput
value.
Using our example of the native implementation near the top of this article, we could do the following to achieve the same results:
constnumbersToAdd=[1,2,3];functionreduce(collection,reducerFn,initialValue){letoutput=initialValue;constclone=[...collection];for(letindex=0;index<clone.length;index++){output=reducerFn(output,clone[index],index,clone);}returnoutput;}functionadditionReducer(previous,current){returnprevious+current;}constresult=reduce(numbersToAdd,additionReducer,0);console.log(result);// 6
Conclusions
Reducers can be quite a complex topic to discuss but just remember, a reducer merely reduces a collection to a single value. That value could be anything you want it to be but that is all it does. I love using reducers in my day to day work as they can make complex tasks much easier andlibraries such as Redux use reducers as a core part of their functionality to do some real heavy lifting. Reducers are also useful for mundane tasks though such as ouradditionReducer
example and so you can adapt them to many use cases quite easily. In saying this though you do want to scope reducers to highly specific use cases and they should adhere strictly to the Single Responsibility Principle as with any function or method implementation.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse