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

4

19

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.

Newcomb answered 2/8, 2017 at 13:25 Comment(1)
Here is the github ticket regarding this question: github.com/lodash/lodash/issues/3298Ribera
B
15

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.

Belia answered 2/8, 2017 at 15:15 Comment(4)
I know lodash/fp is another way. But my question is about lodash-es. If we can't use chain with lodash-es, why is it published? Unless it's a design problem to expose it.Newcomb
lodash-es is more about allowing tree-shaking if you only need access to specific utility functions. Chaining cannot be properly implemented in a modular fashion, as it would severely complicate the code-base if it were to still include lazy evaluation optimizations, and I suspect that would require hard-coding every possible combination of explicitly imported functions in order to be modular, which isn't feasible. Therefore it imports the entire functionality of lodash.Youngster
You can still use _.mixin, I'd really suggest you read the article in my answer :)Belia
In fact, I've read the article before. Based on our discussion, since chain has no valid usecase under lodash-es, it seems it could be a design flaw for lodash-es. I may raise an issue on github.Newcomb
Y
1

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.

Youngster answered 2/8, 2017 at 13:38 Comment(6)
it's the same, see here: github.com/lodash/lodash/blob/es/lodash.jsNewcomb
@Newcomb no, it's not the same. 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.Youngster
But with build tools with tree shaking, wouldn't it be the same?Newcomb
@Newcomb yes, but since your question was specifically about what the proper syntax would be to apply tree-shaking without the help of bundling tools, this is my answer. If you're fine relying on the tools you have to do the tree-shaking for you at the transpiling stage, then your question isn't really a question, or you need to be more clear about what it is you want to know.Youngster
@Newcomb I hope my update addresses the question sufficientlyYoungster
Thx for detail explanation. But it doesn't have any solution. You can see @ulysee-bn 's answer and my comment.Newcomb
R
1

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()
Reis answered 14/7, 2022 at 16:35 Comment(0)
S
1

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.

Seymour answered 26/2 at 13:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.