Research collaborate build

Feb 11, 2020

Things to keep in mind when using Lodash

The first in a three part series about Lodash. This part covers the pros and cons of using the library and other things to be mindful about when using it.
Rishabh Karnad
Rishabh KarnadAssociate Engineering Manager
lines
Lodash is an incredibly popular utility library for JavaScript. If you’re writing an application in JS, chances are you’re already using it. If not, you may consider adding it in the near future.

Here are some things you should keep in mind if you use or want to use Lodash in your app.

Why you should use Lodash

Lodash provides all kinds of utilities. For this reason, you won’t need to spend a lot of time writing your own helpers and tests for the same.

Lodash utilities are also 'safe', meaning they will try their best to handle edge cases and not throw errors. To demonstrate, let's say you need to map over an array of products to get an array with just their codes.

You could use JS’s Array.prototype.map.

Hire our Development experts.
const products = [
  {code: ‘00123’,name: ‘Milk’ },
  {code: ‘01020’,name: ‘Bread’ },
  {code: ‘12232’,name: ‘Eggs’ },
]

const productCodes = products.map(product => product.code)

// productCodes -> [‘00123’, ‘01020’, ‘11232’]

The problem is that if the array you need to apply it on isn’t coming from a predictable data source, say from an external API, and if you don’t correctly handle that edge case, it could crash your application with a dreaded error message.

Hire our Development experts.
const products = await SomeAPI.fetchProducts() //-> null

const productCodes = products.map(product => product.code)
//-> Uncaught TypeError: Cannot read property 'map' of null

This problem could be mitigated if you used Lodash’s map. This map is very similar to Array.prototype.map.

Hire our Development experts.
const productCodes = _.map(products, product => product.code)

If products was null or undefined or a primitive, it would just return an empty array. Since any code using your productCodes array would be expecting an array, everything from this point on would continue working without crashing.

Similarly, fetching nested data is really easy with Lodash's get.

Hire our Development experts.
const someObject = { a: 10, b: 'foo', c: {} }
const p = someObject.c.d.e // Throws an error
const q = _.get(someObj, 'c.d.e') //-> undefined
const r = _.get(someObj, 'c.d.e', 'defaultValue') //-> defaultValue

To sum up, Lodash is great for two reasons:

  1. It provides a lot of helpers that you will frequently use.
  2. It handles errors in its own way, that helps keep your own code a little cleaner.

Chaining operations

Although Lodash does help you write cleaner code by wrapping things like loops and conditionals inside functions with readable names, sometimes you may need to perform a sequence of operations on some data, processing it in stages.

Let’s say you have a list of customers. This list is represented by an array of objects.

You need to get a list of unique cities that the customers, between ages 21 and 30, belong to, so that you may then visualise those numbers on a map, displaying the cities in which your product is most popular.

  • You may need to first filter the list of customers to get those with the correct age.
  • Then, you'll map over those customers, to get their addresses.
  • Then you may need to extract the city from the addresses, which are themselves objects.
  • Then you would need to find the unique cities in that list.

You would probably write that code like so.

Hire our Development experts.
const customers = await someApiToFetchCustomers()
const customersInAgeRange = _.filter(customers, c => c.age >= 21 && c.age <=30)
const customerAddresses = _.map(customersInAgeRange, c => c.address)
const customerCities = _.map(customerAddresses, ‘city’)
const uniqueCities = _.uniq(customerCities)

This quickly becomes cumbersome. You need to create a new variable at each step. You also need to name them, which is probably the second hardest problem in computer science.

If you were using vanilla JavaScript, you could use the Array prototype methods to an extent and chain them, as long as the previous one returned an array at every stage:

Hire our Development experts.
const customerCities = customers
  .filter(c => c.age >= 21 && c.age <= 30)
  .map(c => c.address.city) // May explode in your face with that annoying error, if address is undefined

const uniqueCities = Array.from(new Set(customerCities)) // convert to a set and back for unique values

But what if you wanted to achieve the same thing in Lodash and reap all the benefits?

To do this, Lodash provides an extremely useful function called chain. You could pass anything to chain, and it would return something that would contain the value that you passed, and methods for everything Lodash could do with that value.

Hire our Development experts.
const uniqueCities = _.chain(customers)
  .filter(c => c.age >= 21 && c.age <= 30)
  .map(‘address.city’) // I could have used this shorthand earlier. It combines _.map and _.get
  .uniq()
  .value()

How cool was that? At this point you really begin to see how clean your code can get.

But how does this even work?

Well, chain kind of wraps your value inside an object, whose methods are the functions of Lodash, with the wrapped data as the first argument.

In the end, to get back the result, you would need to extract that wrapped value with value().

Sometimes it may harm you to use Lodash

It's true that Lodash functions are 'safe', so they won't throw errors easily. At the same time this can lead to logical errors that aren't detected simply because they did not throw errors.

Relying too much on Lodash could lead to poor programming practices and less thought towards error cases and how they should be handled.

Secondly, the implicit handling of edge cases by Lodash helpers could take the ability to handle errors out of your hands. Whether your code produces an array, an object, null or undefined, all those things mean something. But the helper, which is concerned with making things easier and more generic, coerces all of these things into one, in order to give a consistent output, even if it’s the wrong thing. Information is lost in this process.

In other words, since you aren't thinking too much about error handling anymore, you could end up with code that doesn't crash, but isn't correct either.

Execution time

Lodash functions take into account several data types and edge cases. As a result, they are sometimes large, end up calling other small utilities, or calling themselves recursively.

As a result, Lodash utilities can sometimes end up being quite a lot slower than their standard library counterparts, although this isn't always true (Lodash has optimisations for chained operations on very large collections).

Bundle size

The code you write isn’t always what finally runs in the browser or phone. Modern JS frameworks (and boilerplates like create-react-app) use build pipelines that typically do the following:

  1. Transpile code (for example from fancy ES7 to universally runnable ES5)
  2. Eliminate dead code, a.k.a. tree-shaking. This dead code could include functions that have been defined but never used or if-statements that turn out to always be true
  3. Finally, minify (remove stuff like extra whitespace and make variable names smaller)

The first step ensures the code can run on as many platforms as possible. Compilers like Babel handle this.

The second and third steps help to actually reduce the bundle size. They are handled by bundlers like Webpack and minifiers like Terser respectively.

Smaller bundle sizes mean smaller amounts of data flowing over the internet.

Less data means quicker load times.

The second step is where you could get in trouble with Lodash. Most tools that perform tree shaking, like Webpack and Rollup can pretty easily eliminate functions that are defined but not used.

Objects are hard though. JS objects are dynamic. So you can add and remove stuff like data and methods to objects on the fly at runtime. Due to the dynamic nature of objects, most tree shaking mechanisms either find it difficult to do it on objects, or simply don't bother doing it due to this fact. (Some part of the code may, after all, try to dynamically access a method on an object while iterating over its keys at runtime, for example. It's difficult to reason what parts should be eliminated.)

If you import Lodash like this:

Hire our Development experts.
import _ from 'lodash'

you are importing ALL of Lodash. _, the default export of Lodash, is an object. _.map, _.filter, _.get and everything else are properties on this object, and your bundler won't be able to eliminate the stuff you don't use.

To prevent this, be careful with your imports.

Hire our Development experts.
import { map, filter, get } from 'lodash'

Or if you have control over your build pipeline, use some plugin that can automatically optimise the imports at build time like babel-plugin-lodash.

Most importantly, if you import chain, you end up implicitly importing all of Lodash and plugins won't help at all.

Hire our Development experts.
import { chain } from 'lodash' //-> Ends up being more or less the same as `import _ from 'lodash'` in terms of bundle size`
 

Prelude to part 2

Lodash has a lesser known module called fp, which has almost all the utilities provided by Lodash, but each with a small twist that greatly enhances their capabilities.

fp here stands for functional programming. The functions in lodash/fp are designed to be used in a more functional style, with a particular emphasis on partial application.

The next part in this series will cover how you can use lodash/fp to your advantage, and even use a function called flow, as an alternative to chain, while overcoming bundle size issues.

TL:DR

Here’s a quick recap of what we covered:

  1. Lodash utilities are generally ‘safe’, meaning they will try their best to give a consistent output and not throw errors. But if things do go wrong, the problem won’t be obvious to spot.
  2. Lodash is pretty large, so adding it as a dependency could drastically increase your bundle size. Either import only that which you need to use or use a plugin which will help automate this in the build process. Alternatively, install only the functions you need as individual packages.
  3. Lodash lets you chain together operations using chain. Importing this, however, effectively imports all of Lodash, so even the best Babel plugin won’t be able to trim down your bundle size.
  4. Lodash is best used as a helper tool. Don’t become too reliant on it.

 

Afterword

Although it’s fun to find out cool stuff about development tools and libraries, and even more fun to put them to good use, it’s important to remember that as devs our goal should be to:

  • Make applications for users and work towards a better experience for them.
  • Write code, keeping in mind that others work with you too; so write clear code that’s simple and readable.
  • Pay attention to performance, but only optimize as the last step in development. Maintainability is important too so carefully balance it with optimizations, since the two tend to be at cross purposes.
Hire our Development experts.