DEV Community

loading...
Cover image for It's time to let go of lodash

It's time to let go of lodash

jmitchell38488 profile image Justin Mitchell Originally published at thejs.dev ・5 min read

In today's world of JavaScript, there's a huge number of tools that solve the same problem in slightly differing ways. Some of the tools are driven by simplicity, some driven by configurability, and others by extensibility. Lodash is one of those handful of libraries, that were created to solve a growing problem, that fortunately due to Microsoft's foresight, no longer exists.

I'm talking of course about Internet Explorer 11.

At the time lodash was created, ES6 was just a glimmer in the eye of web developers, an idea and a desire for most front end developers. In the world of Bootstrap, jQuery, underscore, prototype and a huge number of other tools, lodash provided a consistent and clear interface. Classes, functional programming, and all the goodness and richness that we take for granted was years away.

Let's jump forward to today; we're living in the days of ES11. Browsers are no longer competing on the same standard. The three most used browsers in the world use Webkit or Chromium, standards are consistent and there's very little difference in adoption and behaviour across most browsers. We don't have to tailor our JavaScript or CSS to the version of IE, or Safari, or Firefox, or even OS, because it's all a little difference.

We're now able to just do once, and it do it well. Efficiency has never been better. But that IE11 troll still lives in the cave over there, and we need to find a way to feed it, and avoid it. Fortunately, we can rely on core js and babel, transpilers that spit out ES5 compatible JavaScript that IE11 through to IE Edge 18 can support.

Which brings us neatly to the topic of the blog - do we need lodash anymore?

Do we need lodash?

Transpilers, compilers and build tools try to solve the file size bundling issue in different ways, such as webpack using tree shaking to identify code that isn't used. But lodash is stubborn, it's a painful library to reduce in file size. Even using something as simple as .get imports a significant amount of code. Why? It depends on itself, tools within its own library. cloneDeep is another utility that adds a significant amount of code.

There are more than 100 utility helpers and the majority of them aren't required anymore. Babel is the industry standard for cross-compiling ES6+ code into ES5 compatible code for older browsers. In time, that won't be required with, once users on legacy browsers have been migrated off.

Tuning your build to reduce lodash bloat

There's few options that developers have to reduce the bloat that lodash brings into your compiled build. But there are several strong options:

  1. Import only the functions you need through lodash/* or lodash.*:
const _ = require("lodash"); // typical library import that will bring in the lodash base
const get = require("lodash.get"); // import the specific tool you need, needs to be imported through package.json
const get = require("lodash/get"); // import the specific tool from the lodash base library
Enter fullscreen mode Exit fullscreen mode
  1. Replace lodash Array/Object helpers with native functions:
const myArray = [];

// Lodash array fill 10 elements
const fill = require("lodash/fill");
fill(myArray, {foo: "bar"}, 0, 10);

// Native array fill 10 elements
myArray.fill({foo: "bar"}, 0, 10);
Enter fullscreen mode Exit fullscreen mode
  1. Assess if you really need some of those handy utility functions - do you really need a cloneDeep or merge? Object spread operator can do something very similar with minimal code to merge objects. You don't even have to do Object.assign anymore.
// Merging objects
const myArray = [...[1, 2]]; // [1, 2]
const myFoo = { ...{foo: "bar"}, bar: "foo"}; // {foo: "bar", bar: "foo"}
const myFoo = { foo: "bar", ...{foo: "qwerty"}}; // {foo: "qwerty"}
Enter fullscreen mode Exit fullscreen mode
  1. Array functions are supported across all browsers and replicate a lot of lodash's functionality
const myArray = [1, 2, 3];
// Map
console.log(myArray.map(val => val * 2)); // [2, 4, 6]

// Reduce
console.log(myArray.reduce((acc, val) => acc + val)); // 6

// Entries
const it = myArray.entries();
for (const entry of it) {
    console.log(entry);
    // [0, 1]
    // [1, 2]
    // [2, 3]
}

// Find
console.log(myArray.find(val => val === 3)); // 3

// Filter
console.log(myArray.filter(val => val > 1)); // [2, 3]

// Includes
console.log(myArray.includes(3)); // true

// Cast array
console.log([1]); // [1]

// Unique
console.log([...new Set([1,1,2,3])]); // [1,2,3]
Enter fullscreen mode Exit fullscreen mode
  1. Use babel or core js to compile ES6+ code into ES5 compatible code for older browsers
  2. Make use of JavaScript core functionality to do type inference and checking:
_.isArray([]); //true
Array.isArray([]); //true

_.isNull(null); //true
null === null; //true

_.isObject({}); //true
{} !== null && typeof {} === "object" && Object({}) === {}; //true

_.isBoolean(true); //true
Boolean(true); //true
Enter fullscreen mode Exit fullscreen mode
  1. Use JavaScript functions for Math and Date:
_.now(); //1613434837495
Date.now(); //1613434837495

_.add(6, 4); //10
6 + 4; //10

_.ceil(4.1); //5
Math.ceil(4.1); //5

_.max([1,2,3]); //3
Math.max(...[1,2,3]); //3
Enter fullscreen mode Exit fullscreen mode
  1. Use JavaScript object functions:
_.assign({a: 0}, {a: 1}, {b: 2}); //{a: 1, b: 2}
{a: 0, ...{a: 1}, ...{b: 2}}; //{a: 1, b: 2}

_.omit({a: 0, b: 1}, ["a"]); //{b: 1}
const {a, ...obj} = {a: 0, b: 1}; //a=0, {b: 1}

_.pick({a: 0, b: 1, c: 2}, ["a", "c"]); //{a: 0, c: 2}
const {b, ...obj} = {a: 0, b: 1, c: 2}; //b=1, {a: 0, c: 2}
Enter fullscreen mode Exit fullscreen mode
  1. Use JavaScript collection commands:
_.forEach([1, 2], val => {});
[1,2].forEach(val => {});

_.find([1, 2], val => {});
[1,2].find(val => {});

_.filter([1,2], val => {});
[1,2].filter(val => {});

// Partition
_.partition([{foo: "bar", active: true},{foo: "foo", active: false}], val => val.active); 
// objects for [["bar"],["foo"]]

const partition = [];
[{foo: "bar", active: true},{foo: "foo", active: false}]
    .forEach(val => {
        if (!Array.isArray(partition[Number(!!val.active)])) partition[Number(!!val.active)] = [];
        partition[Number(!!val.active)].push(val);
    });
// objects for [["bar"],["foo]]
Enter fullscreen mode Exit fullscreen mode

Summary

Do we need lodash today? Not really, no. Core JS and Babel do a fantastic job in compiling ES6+ code to ES5 compatible code. A lot of the cross-browser compatible functionality that lodash offers is now easily replicatable in the browser natively. There are lightweight options for merge and cloneDeep, and even native ways of doing cloneDeep, as long as the structure doesn't include functions, such as using JSON.parse(JSON.stringify(original)).

Specific functionality can be brought in on a needs-basis, but other functionality, such as get can easily be created to be project and application specific.

While lodash offeres a tremendous library of functionality, much of it is no longer required, and the cost of importing lodash into your application can be huge, well over 600kb if your compiler doesn't shake the unrequired code, or you use lodash throughout your application.

Pre ES6, lodash was an absolute gem, just like jQuery was back in its day, providing rich cross-browser functionality. These days, all the browsers treat JavaScript standards fundamentally the same, with very little difference.

The days of JScript, Trident and Chakra are long gone, let's not continue to use tools from those days, go native!

Discussion (16)

pic
Editor guide
Collapse
michaelcurrin profile image
Michael • Edited

Please don't recommend Boolean as boolean type check.

Boolean(true);
// true
Boolean(false);
// false
Enter fullscreen mode Exit fullscreen mode

With your approach, you'd incorrectly conclude that a variable of false passed to Boolean() is not a boolean.

Boolean will cast to boolean.

Boolean(1)
// true
Enter fullscreen mode Exit fullscreen mode

Since non-zero numbers are truthy, they will give you a response of true even though not boolean type.

You should use

typeof 1 === 'boolean'
// false

typeof true === 'boolean'
// true
typeof false === 'boolean'
// true
Enter fullscreen mode Exit fullscreen mode
Collapse
michaelcurrin profile image
Michael

Bonus points if use you TypeScript

function hello(greet: Boolean) {}
Enter fullscreen mode Exit fullscreen mode
Collapse
jmitchell38488 profile image
Justin Mitchell Author

Explicitly checking truthiness is better than implicitly checking, and to do that you need to use strict equality true/false, as all other checks are implicit by means of coercing a value to a bool.

Thread Thread
michaelcurrin profile image
Michael • Edited

Assuming your comment was on my previous comment and not the TypeScript one, as TypeScript will check type is a boolean for you using myVar: Boolean.

Use this to check for type boolean:

typeof myValue === 'boolean'
Enter fullscreen mode Exit fullscreen mode

Use this to check if a value is actually true and boolean, not just truthy. Because triple equals sign does a type check

myVar === true
Enter fullscreen mode Exit fullscreen mode
1 === true
// false
Enter fullscreen mode Exit fullscreen mode

Beware the double equals sign which coerces ie is checks for truthiness and also handles strings funny.

5 == "5" // true

0 == false // true
Enter fullscreen mode Exit fullscreen mode

I don't see a common reason to use Boolean().

It adds change in logic here so is verbose.

// this checks for truthiness twice 
if (Boolean(myVar)) {}

// and equivalent to just this which checks once.
if (myVar) {}
Enter fullscreen mode Exit fullscreen mode

If you want to be explicit in logic flows use triple equals and a value.

// true for 1. and false for 0 and 1000 and any non Number type
if (myVar === 1) {}

// true for true only and false for everything else.
if (myVar === true) {}
Enter fullscreen mode Exit fullscreen mode

You could use Boolean() to convert to a Boolean type but I can't remember needing to do this in JS.

var myNumber = 1 // or 1000 or 0 or -999 etc.
var myBool = Boolean(myNumber) // can be true. Or false for myNumber equal to zero

var a = Boolean("my string")
// true
var b = Boolean("")
// false
b === false
// true
Enter fullscreen mode Exit fullscreen mode

Boolean docs

Thread Thread
jmitchell38488 profile image
Justin Mitchell Author • Edited

Relying on TS to ensure type checking is fraught with danger esp with input data. TS isn't a runtime environment, it's just a transpiler.

Boolean(a) is equivalent to !!a

Explicit checking requires strict equals, in my previous comment, but you will need to test for true and false.

Thread Thread
michaelcurrin profile image
Michael

Okay thanks. I didn't understand your TS before.

Yes if you have user input you could pass non accepted type to a function. That is a risk for all types and not just boolean and there are a few ways to do that.

Like myVar as Boolean.
Or if your input can't be trusted, then add some sanitization on your form handling or use a check before or within the function as typeof myvar === 'boolean.

Thread Thread
jmitchell38488 profile image
Justin Mitchell Author • Edited

Like I said earlier, !!prop and Boolean(prop) are fundamentally the same - they determine the truthiness of a value by coercion, not if it is in fact a boolean, just that it has a value other than "", null, undefined, false and 0. Given an array and object share the same super type object, truthiness check will always return true.

typeof myvar === boolean isn't required when you can just do prop === true || prop === false and explicitly check the values, rather than the type.

Lodash explicitly checks the truthiness of the var, and some other checking for arrays and objects. My suggestion isn't a hard and fast rule, it's just one of many options and it's a useful shorthand check. Feel free to use !!var, Boolean(var) or copy+paste the lodash code.

Thread Thread
michaelcurrin profile image
Michael

Ok thanks. My original point was not on the best way to do truthiness though but the fact that these two are not interchangeable as you had suggested, so was recommending it be fixed.

_.isBoolean(true); //true
Boolean(true); //true - same as above line
Enter fullscreen mode Exit fullscreen mode
_.isBoolean(false); //true
Boolean(false); //false. But different result to above line and therefore not equivalent
Enter fullscreen mode Exit fullscreen mode
Collapse
cheerfulstoic profile image
Brian Underwood

I agree with a bit of this. Sometimes it's simple enough to use forEach or map or reduce directly. And honestly, I wasn't familiar with the spread syntax, and that is πŸ”₯

But there is a bit of a strawman argument here. I don't care about cross-browser compatibility (or browser download size so much at the moment given that I'm doing a lot of Node.js). You've skipped past many of the conveniences of lodash which make it quite worthwhile:

  • The ability to chain multiple functions together nicely
  • You wrote a partition alternative, but, please, that is so ugly. Why should we spend our time writing that and struggle getting it right?
  • When using map or each or filter, there is so much convenience is being able to use the simpler forms like _.map(arr, 'foo.bar.bar') or .filter(arr, {key: value}).
  • It makes working with enumerating over objects simpler than plain JS

Just in general, it's nice to have around to do the simple things (like partition, but much more of course) that I shouldn't be spending my time worrying about. And it provides a consistent interface to it all. Your examples about type inference and checking just make me ❀️ lodash even more because, just look at those JS examples. It's a such mismash! πŸ˜ƒ

Thanks for the post!

Collapse
johannel00 profile image
johannel00

I agree. Lodash is totally unnecessary when bundle size matters. Invest more time in learning how specific functions work to save time and resources down the road. Nonetheless, Lodash and jQuery will always have a special place in my heart.

Collapse
jmitchell38488 profile image
Collapse
michaelcurrin profile image
Michael • Edited

Try indent your code so your bullet points number correctly

Below

1. A
Enter fullscreen mode Exit fullscreen mode

You can indent 4 spaces in addition to code fence block.

    ```js
    console.log()
    ```
Enter fullscreen mode Exit fullscreen mode

Result

  1. A

    console.log()
    
  2. B

Collapse
bytebodger profile image
Adam Nathaniel Davis

Lodash and Underscore are two of my common pet peeves. In fact, I was just talking to our team about this last week, because we are porting over old code and I specifically said that I didn't wanna be dragging over all the old Lodash stuff, just because it's an easy copy-n-paste.

Those packages were awesome - 10 years ago. Now, they're mostly superfluous. And they serve as crutches for those who haven't bothered to learn modern syntax.

Collapse
jmitchell38488 profile image
Justin Mitchell Author

Yep, 100% agree

Collapse
michaelcurrin profile image
Michael

Do you think maybe we'll always need something like Babel? So it won't go away.

There are always going to be legacy browsers and users who have to update. When they've all caught up to using ES6 in a few years, by then there is some newer syntax they don't have support for. So you going to need to transpile down to ES6 or ES7, rather than to pre-ES6.

Collapse
jmitchell38488 profile image
Justin Mitchell Author

Babel will be required for users on older browsers that don't support bleeding edge. Es6 and even es7 sugar is no longer transpired unless specifically configured. Null coalesce and optional chaining do get transpiled.