DEV Community

loading...

Lodash chaining alternative

munawwar profile image Munawwar ・Updated on ・3 min read

Those of you dealing with data transforms/manipulations for charting/dashboards/whatever, need no introduction to Lodash library, and have been using it happily on the backend, frontend etc

The problem

However there is one method Lodash has that's so useful but has its performance implications on the frontend.. namely chain().
On the frontend if you naively import chain the entire lodash library will end up in your bundle.. and the entire lodash library isn't small. Backend code doesn't care about some extra bloat.

That's sad. chain is very useful and I would like to use chaining on the frontend without having to take a performance hit. So.. what is the solution?

What does Google say?

Googling around you would see many suggestions to use lodash/fp's flow() method. You can see the code from this 2016 post

import map from "lodash/fp/map";
import flatten from "lodash/fp/flatten";
import sortBy from "lodash/fp/sortBy";
import flow from "lodash/fp/flow";
flow(
  map(x => [x, x*2]),
  flatten,
  sortBy(x => x) 
)([1,2,3]);
Enter fullscreen mode Exit fullscreen mode

It works.. it keeps the bundle size small and gives you chaining capability.

But there is something bothersome about that code...

_.chain begins with the data you need to manipulate and then you call the transforms.. whereas flow() begins with the transforms and ends with the data you want to manipulate. This isn't natural to read. It needs to be flipped around.

[From flow()'s perspective it is built as intended. flow is potentially built for reuse. Fine. However we still miss a closer alternative to chain.]

Better solution

My ideal syntax would be the following:

chain([1,2,3])
  (map, x => [x, x*2])
  (flatten)
  (sortBy, x => x)
  ();
Enter fullscreen mode Exit fullscreen mode

However most linter configurations would complain about the indented parentheses. So we need a dummy function and a .value() to break out of the chain (like lodash already does)

chain([1,2,3])
  .fn(map, x => [x, x*2])
  .fn(flatten)
  .fn(sortBy, x => x)
  .value();
Enter fullscreen mode Exit fullscreen mode

Overall if you squint your eyes and ignore the .fn()s, then it looks very similar to lodash's _.chain syntax. And there is a way to implement this. I'll dive straight into the implementation which is small and probably don't need too much explanation:

function chain(value) {
  return {
    /**
     * @param {function} func function
     * @param  {...any} args
     */
    fn: (func, ...args) => chain(func(value, ...args)),
    value: () => value,
  };
}
Enter fullscreen mode Exit fullscreen mode

This implementation brings some new opportunity considering how generic the approach is.

The function doesn't know anything about lodash. It takes in any function. So you can write custom functions or use Math.* or Object.* functions

chain({prop: 2, fallback: 1})
  .fn((obj) => obj.prop || obj.fallback)
  .fn(Math.pow, 2)
  .value(); // result = 4
Enter fullscreen mode Exit fullscreen mode

Improvement

With a slight modification, we can make it call any function on result objects.

Which mean for arrays, we can use native array map, filter etc, and we don't really need to use lodash's functions there. We should be able to do something like the following:

chain([1,2,3])
  .fn('map', x => [x, x*2])
  // ... blah
  .value();
Enter fullscreen mode Exit fullscreen mode

Instead of passing the function here we put a name of the method to be invoked from the intermediate result object/array. The implementation of fn will change to the following:

    /**
     * @param {function|string} func function or function name (in chained value)
     * @param  {...any} args
     */
    fn(func, ...args) {
      if (typeof func === 'string') {
        return chain(value[func](...args));
      }
      return chain(func(value, ...args));
    },
Enter fullscreen mode Exit fullscreen mode

I believe this is an improvement to the popular approaches suggested out there on the interwebz. Check it out, try it out.. criticism welcome.

That's all folks. Hope you liked my short, hastily written post.

Full code below:

function chain(value) {
  return {
    /**
     * @param {function|string} func function or function name (in chained value)
     * @param  {...any} args
     */
    fn(func, ...args) {
      if (typeof func === 'string') {
        return chain(value[func](...args));
      }
      return chain(func(value, ...args));
    },
    value: () => value,
  };
}
Enter fullscreen mode Exit fullscreen mode

Discussion (4)

Collapse
miketalbot profile image
Mike Talbot

I think the point of flow is that it defines a proper functional pipeline as a callable, that you will call later with the data. In functional programming, the data is normally last and the flow itself is also chainable and can be included inside a wider functional pipe. Chaining is the other way around. I'd argue that flow in the order it is presented by lodash is perfectly correct and makes total logical sense if you use it to define the function:

const processInputData = flow(
  map(x => [x, x*2]),
  flatten,
  sortBy(x => x) 
)

processInputData([1,2,3])
processInputData([4,5,6])

const complexProcess = flow(
   processInputData,
   map(([a,b])=>[a,b,a*b,a+b])
)

complexProcess([7,8,9])
Enter fullscreen mode Exit fullscreen mode
Collapse
munawwar profile image
Munawwar Author

Got it. Yet it still leaves for a closer alternative for chain for the front end.

Collapse
seanmclem profile image
Seanmclem

but the function still uses chain()

Thread Thread
munawwar profile image
Munawwar Author

Where? That's my own chain implementation not using lodash chain.

Forem Open with the Forem app