Patrick Desjardins Blog
Patrick Desjardins picture from a conference

Dissecting Redux Compose Function

Posted on: 2018-01-23

Redux is one of the most popular state management in the JavaScript ecosystem since few months. In this article, we will dissect the method that composes all middleware named "compose". The role of this method is to aggregate all functions that will have the opportunity to be executed everytime an action is being dispatched. It's a door open to do something before passing to the next function (middleware). It's the only opportunity to modify data before reaching the reducers and the store. The method is used once. It is in the popular method applyMiddleware that we will see in a future article. For the moment, just keep in mind that compose function is taking all middleware to place them in a queue where the data will flow sequentially before reaching

Before analyzing the method, let's look at all of it.

export default function compose(...funcs) { 
  if (funcs.length === 0) { 
    return arg => arg;
  }

  if (funcs.length === 1) { 
    return funcs[0];
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)));
} 

That's it!. And this can be even simpler because the two first ifs are for cases that are rarely used which is composing nothing (no middleware) or composing a single middleware. In the first case, we return a new function that returns the first parameter. For example:

compose()("a", "b", "c") // Return "a" 

This example composes nothing. It might seem that we have 3 parameters, but in fact, compose returns a function that has three arguments. When composing middleware, we will place the middleware inside the compose function, not using the middleware as a parameter of the returned function. This is important to understand that the compose function returns a new function and not a value. The condition statement that looks for a length of one simply return the first parameter of the compose function. This is also rarely used as having a single middleware is rare and wouldn't require using at all the compose function at the first place -- you could just use the middleware directly.

Having the two ifs analyzed let's jump on the last line which is the confusing one. The last line is the line that is mostly to be used because most application using Redux has several middlewares. The first thing to notice is the use of the JavaScript function reduce. The reduce function is applied to an array and call a callback with two parameters which are the accumulated value followed by the value of the array. The callback is executed the same amount of the length of the array. So, if you have 3 middlewares, the reduce function will call three times the callback. Here is a simple example to grasp the concept. The example takes an array and reduces it by summing every number. The callback function is anonymous and will be called 3 times (length-1). The first time the parameter "acc" and "value" will be 0,1; then 1,2; then 3,3. When every element of the array has proceeded, it returns the value which is 6.

console.log([0, 1, 2, 3].reduce( function(acc, value){
   return acc + value; 
   } 
  ) 
); 

The first confusion with the compose function is that the reduce function is returning (...args) which is not clear. It's a little bit simpler to remove the arrow function to use traditional functions. Here is the same function written with functions.

export default function compose(...funcs) { 
  if (funcs.length === 0) { 
    return arg => arg;
  }

  if (funcs.length === 1) { 
    return funcs[0];
  }

  return funcs.reduce(combineMiddleware);
}

function combineMiddleware(functionWrapper, middlewareToAdd){ 
  return function(...nextFunction){ 
    console.log("What is nextFunction:", ...nextFunction); 
    return functionWrapper(middlewareToAdd(...nextFunction)); 
  }; 
} 

The compose function is calling reduce on a named function "combineMiddleware". At that point, it should be clear that compose method returns a function since the reduce function returns an aggregation of an array and that the array is an array of function hence the result will be a single function. What remains unclear is how we aggregate since we won't be summing numbers this time.

The combineMiddleware function will return a new function. Since the reduce function takes the return of the callback for the accumulation, this new function will be used on the next invocation. At that point, invoking compose with 3 functions would return a function returned by combineMiddleware. Nothing would happen. Let's create three fake functions and add some log statements to follow what is really happening.

function compose(...funcs) { 
  if (funcs.length === 0) { 
    return arg => arg;
  }

  if (funcs.length === 1) { 
    return funcs[0];
  }

  return funcs.reduce(combineMiddleware);
}

function combineMiddleware(functionWrapper, middlewareToAdd){ 
  console.log("Combined!!!", middlewareToAdd); 
  return function(...nextFunction){ 
    console.log("What is nextFunction:", ...nextFunction); 
    return functionWrapper(middlewareToAdd(...nextFunction)); 
  }; 
}

const a = function(next){ console.log("Outside A", next); 
return function(previousReturnedValue) { 
  console.log("Inside A", previousReturnedValue); 
  return next(previousReturnedValue + 'a'); 
  }; 
}; 

const b = function(next){ 
  console.log("Outside B", next); 
  return function(previousReturnedValue) { 
    console.log("Inside B", previousReturnedValue); 
    return next(previousReturnedValue + 'b'); 
  }; 
};

const c = function(next){ 
  console.log("Outside C", next); 
  return function(previousReturnedValue) { 
    console.log("Inside C", previousReturnedValue); 
    return next(previousReturnedValue + 'c'); 
  }; 
}; 

const final = function(next){ 
  console.log("Final", next); 
  return next; 
};

console.log("Compose Output", compose(a, b, c)); 

We have the same compose, this time with a console.log that will output what is the next function. There are also three functions that return a function. This is how middlewares are constituted and since the compose function takes a function it is compatible. The idea is that each function takes a next parameter which is a pointer to the "next middleware" allowing the actual middleware, the inner function to be executed and returning the value transformed to the next one. In this example, we compose A-B-C. The compose will wrap the function to have C wrapping B wrapping A which will start with the value passed down (not provided yet). So, it is actually doing: return function(A(B(C("Data here)))). The console output looks like the following:

  //Combined!!! 
  ƒ (next){ 
   console.log("Outside B", next); 
   return function(previousReturnedValue) { 
     console.log("Inside B", previousReturnedValue); 
     return next(previousReturnedValue + 'b'); 
    }; 

  // Combined!!! 
  ƒ (next){ 
    console.log("Outside C", next); 
    return function(previousReturnedValue) { 
      console.log("Inside C", previousReturnedValue); 
      return next(previousReturnedValue + 'c'); 
    }; 

  // Compose Output 
  ƒ (...nextFunction){ 
    console.log("What is nextFunction:", ...nextFunction); 
    return functionWrapper(middlewareToAdd(...nextFunction));
  } 

So, what is nextFunction is the final function passed as a parameter to the returned function of "compose". In Redux, the applyMiddleware function will use it to pass the store.dispatch down (we will see in a future article). As expected, the combineMiddleware method output twice since we have 3 functions. We can see the final output being the function returned by combineMiddleware function. The next function returns the functionWrapper which is the function A, that call the function B with ...nextFunction that is function C.

If change the last output statement to have the compose function execute the returned function, we will see more the flow of the execution.

console.log("Compose Output", compose(a, b, c)); // Change this line by: 
console.log("Compose Output", compose(a, b, c)(final)); 

The output is different this time but calls twice "What is next function" since we have 4 functions having the "final" one that will be wrapped also but executed. The major change is that every function gets called.

  //Combined!!! 
  ƒ (next){ 
    console.log("Outside B", next); 
    return function(previousReturnedValue) { 
      console.log("Inside B", previousReturnedValue); 
      return next(previousReturnedValue + 'b'); 
    }; 

  // Combined!!! 
  ƒ (next){ 
    console.log("Outside C", next); 
    return function(previousReturnedValue) { 
      console.log("Inside C", previousReturnedValue); 
      return next(previousReturnedValue + 'c'); 
    }; 

  // What is nextFunction: 
  ƒ (next){ 
    console.log("Final", next); 
    return next; 
  } 
  
  // Outside C 
  ƒ (next){ 
    console.log("Final", next); 
    return next;
   } 
   
  // What is nextFunction: 
  ƒ (previousReturnedValue) { 
    console.log("Inside C", previousReturnedValue); 
    return next(previousReturnedValue + 'c'); 
  }
  
  // Outside B 
  ƒ (previousReturnedValue) { 
    console.log("Inside C", previousReturnedValue);
     return next(previousReturnedValue + 'c'); 
  } 
  
  //Outside A 
  ƒ (previousReturnedValue) { 
    console.log("Inside B", previousReturnedValue); 
    return next(previousReturnedValue + 'b'); 
  }

  // Compose Output 
  ƒ (previousReturnedValue) { 
    console.log("Inside A", previousReturnedValue); 
    return next(previousReturnedValue + 'a'); 
  } 

The last output is still a function, but this time, not the combine wrapper but the function A. This is interesting since every function was also a function but with a pointer to the next one. It means that that the returned function "A" will be executed and since this one call next (which in that case is "B") the cascade will continue until the "final" function is called. The final function also calls next and for the moment doesn't do much. In fact, we can remove the "return next" from function "final" and to have the same output.

Let's alter another time the last line.

console.log("Compose Output", compose(a, b, c)(final)("!")); 

This time, we execute the composition of functions completely. We are passing down the exclamation point. As we saw in the last trace, the previousReturnedValue is added before the function, hence we can expect to have the result "!ABC";

  // Same trace until the last output Inside A ! Inside B !a Inside C !ab Final !abc Compose Output !abc 

As we notice, the function A is called first and call the function B and C and final. Removing the "return next" from the final function will print undefined.

If we move back to Redux and how it uses the compose function we see that the applyMiddleware use it this way:

dispatch = compose(...chain)(store.dispatch) 

This allows having the consumer of a configured Redux to call dispatch(ACTION) and have action passed down. As we can see, the compose takes "chain" which is an array of the middleware configured and pass down as the "final" the store.dispatch. It means that the store.dispatch will call the result of all the middleware.

In this article, we have dissected the small function compose of Redux. We saw that its goal is to wrap multiple functions and weave them together with a callback that allows chaining the result of each of them to finally be able to execute a final function on the cascade of results. For Redux, it will allow developers to dispatch action to reducers and having the dispatched action to alter the flow between the time the action is executed and persisted.