loading...

You Might Not Need Lodash

antjanus profile image Antonin Januska ・4 min read

You can check out more of my tutorials and articles on my main blog. Enjoy the article!

While Lodash may have become a staple in any JavaScript developer's toolkit, a lot of the methods in it have slowly migrated over to being part of JavaScript itself or rather part of the EcmaScript spec.

Lodash isn't huge, in fact, it's very light, and properly imported and tree-shaken, its size can be negligible but why bother with all of that if you might not need it in the first place?

Here's a collection of my favorite Lodash methods and how to substitute them with ES2015+ native methods. Sometimes the substitution is 1-to-1, sometimes it's not. I'll make sure to note that

Note: Lodash methods tend to be super short and sweet. If you've never looked through an open source codebase, I highly recommend Lodash's github repo

_.toArray: Object.values + Array.from

Simply put, you're converting something into an array. Most commonly, I've used this method to convert a lookup object like so:

const postAuthors = {
  'Antonin Januska': { id: 1, name: 'Antonin Januska', role: 'author' },
  'JK Rowling': { id: 2, name: 'JK Rowling', role: 'real author' },
};

into an iterable array for display purposes. Well now, I can use this method:

const postAuthorsArray = Object.values(postAuthors);

/** result:
[
  { id: 1, name: 'Antonin Januska', role: 'author' },
  { id: 2, name: 'JK Rowling', role: 'real author' }
]
**/

Look up objects can be handy for creating unique lists, aggregating data, and well, looking things up. More often than not, that object then has to be converted into an array to be used for other things.

What about Array.from? Well, _.toArray supports converting other variable types into arrays, not just objects. For those, Array.from makes more sense. Here are some examples:

const dnaStrand = 'gattaca';
const dnaArray = Array.from(dnaStrand); // results in ['g', 'a', 't', 't', 'a', 'c', 'a'];

const someNumber = 3;
const result = Array.from(someNumber); // results in []. Not sure what this is used for but lodash supports this

Unfortunately, that's where the 1-to-1 parity ends. Neither Array.from nor Object.values supports converting nulls into empty arrays.

_.clone: Object/Array spread

Cloning an object or an array is pretty useful. In either case, manipulating the result means that you don't affect the source data. It can also be used to create new objects/arrays based on some template.

JavaScript does not have a shortcut for deepClone so be wary, nested objects are not cloned and the references are kept. Also, cloning an array of objects makes the array safe to manipulated, not the objects themselves.

There are several ways to achieve the same result, I'll stick to object/array spread:

const clonedObject = { ...sourceObject };
const clonedArray = [ ...sourceArray ];

Unlike lodash, utilizing JavaScript's built-in methods requires you to know their type. You can't spread an object into an array and vice-versa to achieve a clone.

.assign/.extend: Object.assign

Assign/extend allow you to essentially "merge" an object into another object, overwriting its original properties (note: this is unlike _.merge which has some caveats to it). I actually use this all the time!

To achieve this without lodash, you can use Object.assign which the lodash docs even reference.

const sourceObject = { id: 1, author: 'Antonin Januska' };

Object.assign(sourceObject, {
  posts: [],
  topComments: [],
  bio: 'A very cool person',
});

/** result:
{
  id: 1,
  author: 'Antonin Januska',
  posts: [],
  topComments: [],
  bio: 'A very cool person',
}

note: this is still sourceObject
**/

Object.assign will fill use the 2nd (3rd, 4th, etc.) arguments to fill in the sourceObject.

What if you want the result to be a new object and keep immutability? EASY, just specify an empty object as the first argument!

const sourceObject = { id: 1, author: 'Antonin Januska' };

const finalObject = Object.assign({}, sourceObject, {
  posts: [],
  topComments: [],
  bio: 'A very cool person',
});

// note: sourceObject is a separate object from finalObject in this scenario

In fact, before object spread, you'd just use Object.assign({}, whateverObject) to do a shallow clone.

Bonus: _.flatten: Array.smoosh

Flatten is being considered to be part of EcmaScript but due to various problems and issues, there has been a (joke?) nomination to rename it smoosh. I have my own thoughts on the matter but hopefully, sometime soon, you'll be able to use Array.smoosh on your favorite deeply nested arrays.

So what does flatten/smoosh do? It takes an array of arrays, and makes it a single array. So let's say some API looked at your Twitter lists and picked the best tweets from each list and you wanted to combine them into a feed of its own, you might use flatten for this:

const sourceArray = [
  [ 'tweet 1', 'tweet 2', 'tweet 3'],
  [ 'tweet 4', 'tweet 5'],
  [ 'tweet 6', 'tweet 7', 'tweet 8', 'tweet 9']
];

const feed = Array.smoosh(sourceArray);

/** result:
[ 'tweet 1', 'tweet 2', 'tweet 3', 'tweet 4', 'tweet 5', 'tweet 6', 'tweet 7', 'tweet 8 ', 'tweet 9' ];
**/

Discussion

pic
Editor guide
Collapse
gsonderby profile image
Gert Sønderby

Extremely quick and dirty homebrew of a flatten function:

const flatten = array => array.reduce(
  (flatArray, item) => {
    if (Array.isArray(item)) {
      return flatArray.concat(flatten(item));
    } else {
      return flatArray.concat([item]);
    }
  },
  []
);

EDITED: Return a fresh array instead of modifying flatArray. Clearer, less prone to nitpickery.

Collapse
masaeedu profile image
Asad Saeeduddin

It's a little confusing to simultaneously use reduce and push. Use reduce when you're actually computing a new value, use a for of loop when you're mutating things in a loop.

// Use this
const flattened = as => as.reduce((p, c) => [...p, ...c], [])

// Or this
const flattened = []
for (const item of as) {
  flattened.push(...item)
}
Collapse
gsonderby profile image
Gert Sønderby

Your first example chokes on [1,[2,3,[4]],5,[6,7]], throwing an error. Worse, on ['yo',['dawg','I',['herd']],'you',['like','arrays']] it breaks in a number of interesting ways, spreading some strings, failing to spread some arrays.

Thread Thread
masaeedu profile image
Asad Saeeduddin

It is an implementation of the join function for arrays, and works with arrays of arrays, not arrays of mixed objects.

If you need it to work with mixed objects you'll need a recursive call (or ap+pure):

const flattened = as => as.reduce((p, c) => [...p, ...(Array.isArray(c) ? flattened(c) : [c])], [])

Anyway, the point is to avoid mutation in the reduce, whatever it is you may be implementing.

Thread Thread
gsonderby profile image
Gert Sønderby

At this point it honestly seems like you're just trying to score points against me, in some way. I am disinterested in continuing that.

Thread Thread
masaeedu profile image
Asad Saeeduddin

Sorry to hear you feel that way. I was just suggesting that using mutation in reduce is confusing, didn't mean for it to turn into this long-running back and forth.

Collapse
gsonderby profile image
Gert Sønderby

The array I am pushing to is the accumulator of the reduce call, though. I'm pushing the elements of each array found, once flattened.

It works like a classic head/tail recursion, if an element is an array it first gets flattened itself, then it's elements are pushed onto the accumulator.

Thread Thread
masaeedu profile image
Asad Saeeduddin

That's fine, but as I said, it's confusing to use reduce and mutation simultaneously. You're passing [] as a seed value, so the only thing that got mutated was that seed array. If you'd used array.reduce((flatArray, item) => ...) without the seed value (which would be fine but for the mutation), it would end up filling the first item in the input array you were passed.

In general it's easier on the fallible human developer to make mutation look like mutation and pure computation look like pure computation.

Thread Thread
gsonderby profile image
Gert Sønderby

The only way it would matter here is if you somehow changed the function to make flatArray available while the function was running. I'm not even sure how you'd do that. But I do want to point your attention to the words, used above: "quick and dirty"...

Collapse
antjanus profile image
Antonin Januska Author

Yeah, I was thinking about posting code snippets that replace Lodash but then I quickly realized that Lodash is SUPER lightweight to begin with and can do this stuff much better than any snippet I can write. Plus docs/tests/code coverage/edge case solutions are part of using Lodash.

You can check it out here and for convenience, here's a copy:

    function baseFlatten(array, depth, predicate, isStrict, result) {
      var index = -1,
          length = array.length;

      predicate || (predicate = isFlattenable);
      result || (result = []);

      while (++index < length) {
        var value = array[index];
        if (depth > 0 && predicate(value)) {
          if (depth > 1) {
            // Recursively flatten arrays (susceptible to call stack limits).
            baseFlatten(value, depth - 1, predicate, isStrict, result);
          } else {
            arrayPush(result, value);
          }
        } else if (!isStrict) {
          result[result.length] = value;
        }
      }
      return result;
}
Collapse
gsonderby profile image
Gert Sønderby

I admit I hesitate to call that bit lightweight, at least in terms of reading and understanding it.😀

Your point re. testing and edge cases and so forth is well seen. My example was mainly to give an alternative where a native function does not currently exist.

Thread Thread
antjanus profile image
Antonin Januska Author

Lightweight in size! Especially if you import only what you need. eg:

// import directly what you need
import clone from 'lodash/clone';

// or if you have tree shaking
import { clone } from 'lodash';

// or if you use the lodash babel plugin
import _ from 'lodash';

//and then use it!

Reading it isn't too bad but their looping is bizarre to me. It's done for memory/speed use but counting down from a number in a while loop instead of doing a for loop baffles me but I understand they have reasons for it.

Collapse
mlg profile image
m-lg

Quick way to flatten 1 deep
const feed = [].concat(...sourceArray)

Collapse
nicolasnarvaez profile image
Nicolás Narváez

i do the same ;)

Collapse
antjanus profile image
Antonin Januska Author

wow. That's awesome! It took me a few to figure out how this works.

Collapse
paulasantamaria profile image
Paula Santamaría

Great article! Whenever I feel like I need to add Lodash to a new project I try to solve my immediate needs with plain Javascript first. Turns out most of the times that's enough.
It's an awesome library, but I like to keep my projects as simple as possible.

Collapse
stevealee profile image
SteveALee

You can also use mutlple sources in the RHS of a spead expression to get a merge

Collapse
antjanus profile image
Antonin Januska Author

do you mean like this?

const mergedObj = { ...obj1, ...obj2 };

My only issue with it is that it doesn't recursively merge. It's fine for flat objects. I use this pattern pretty often at work, sometimes in conjunction with lodash.

For instance, I use Objection for ORM and it has "computed properties" that need to be accessed in order to get the value, they're not serialized when doing toJSON so I often resort to:

res.send({ ...someModel, ..._.pick(someModel, ['computedProperty1', 'computedProp2']);

EDIT Totally forgot, this is a pretty common pattern in Redux!

Collapse
stevealee profile image
SteveALee

Completely agree. Its shallow like Object.assign. I was just say is an alternative to it really.

Collapse
whiteadi profile image