e-cloud
e-cloud

Reputation: 4471

How to use `chain` with `lodash-es` while supports tree shaking?

As we all know, lodash-es is built with a more modular syntax for supporting tree shaking by build tools.

However, chain related features means some functions are attached to a object/prototype chain.

I can see chain is published with lodash-es, but I am not sure how to use it with proper imports with other chained method.

A usecase may look like this:

import { chain } from 'lodash-es'

export function double(input) {
    return chain(input)
        .without(null)
        .map(val => val * 2)
        .value()
        .join(', ')
}

Edit #1:

The point is not about how is chain imported, but about how are other chained functions imported.

Upvotes: 21

Views: 11585

Answers (4)

Thor Galle
Thor Galle

Reputation: 123

Alternative flow solution with lodash-es/flow

I went with another approach, which doesn't rely on modifying lodash internals, and still keeps tree shaking.

import { flow, map, flatten, sortBy } from "lodash-es";

export const f = <V, I, R>(func: (v: V, i: I) => R, i: I) => (v: V) => func(v, i)

const value = flow([
    f(map, x => [x, x*2]),
    flatten,
    f(sortBy, x => x)
])([3,1,2])

The idea is to manually wrap each function that normally takes (value, iteratee) parameters with a higher-order function f() that also accepts the iteratee, and outputs a value-first equivalent. In other words, you convert the function to its lodash/fp behavior.

This adds a little extra syntax, but you might find it readable enough.

Upvotes: 1

Pyrolistical
Pyrolistical

Reputation: 28062

I found simpler, but tricker answer on how to build your own chain.

import * as ld, { wrapperLodash as _ } from 'lodash-es'

ld.mixin(_, {
  chain: ld.chain,
  map: ld.map
})
_.prototype.value = ld.value

const emails = _.chain(users)
  .map('email')
  .value()

Upvotes: 1

Ulysse BN
Ulysse BN

Reputation: 11396

EDIT: as pointed out by Snook, there has been work on a github issue on this subject. So I've added this to my answer. Go to Flow solution for the previous answer (which is as good IMHO).

Custom chain solution

import map from 'lodash-es/map';
import filter from 'lodash-es/filter';
import mapValues from 'lodash-es/mapValues';
import toPairs from 'lodash-es/toPairs';
import orderBy from 'lodash-es/orderBy';
import groupBy from 'lodash-es/groupBy';
import sortBy from 'lodash-es/sortBy';

// just add here the lodash functions you want to support
const chainableFunctions = {
  map,
  filter,
  toPairs,
  orderBy,
  groupBy,
  sortBy,
};

export const chain = (input) => {
  let value = input;
  const wrapper = {
    ...mapValues(
      chainableFunctions,
      (f) => (...args) => {
        // lodash always puts input as the first argument
        value = f(value, ...args);
        return wrapper;
      },
    ),
    value: () => value,
  };
  return wrapper;
};

There is also a TypeScript version available at lodash/lodash#3298.

Flow solution

You can't, chain needs to bundle all (or most) lodash's functions.

You can use flow though. Here is an example of converting this:

import _ from "lodash";

_.chain([1, 2, 3])
 .map(x => [x, x*2])
 .flatten()
 .sort()
 .value();

into this:

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]);

This example (and more) come from this article.

Upvotes: 16

Patrick Roberts
Patrick Roberts

Reputation: 51886

New answer

In chain.js, you see the first line is

import lodash from './wrapperLodash.js';

If we go to that file, we'll find a long explanation about how chaining is implemented using lazy evaluation that can shortcut iteratees until the call to value(). Below that is an exported helper function defined like this:

function lodash(value) {
  if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) {
    if (value instanceof LodashWrapper) {
      return value;
    }
    if (hasOwnProperty.call(value, '__wrapped__')) {
      return wrapperClone(value);
    }
  }
  return new LodashWrapper(value);
}

Going back to chain.js, we see how that is used in the chain() function:

function chain(value) {
  var result = lodash(value);
  result.__chain__ = true;
  return result;
}

Essentially, chain() checks the input to make sure it's not already a wrapped value, and if it is, it either returns the value if it's an instance of the correct class, or it returns a new wrapped value.

There are no methods attached to any native prototype chains in this implementation, but it does create a new class called LodashWrapper that wraps the input object with lodash functionality and lazy evaluation optimizations.

Old answer

I believe the correct import statement to apply tree-shaking would be

import chain from 'lodash-es/chain'

This imports the same module to the same variable as the import statement used in the question, but the difference is that running import { chain } from 'lodash-es' evaluates all of the imports in lodash.js, whereas my import method only touches the chain.js file and whatever its necessary dependencies are in wrapperLodash.js.

Upvotes: 1

Related Questions