Return to Blog

4 JavaScript Libraries to Learn in 2021Article by Brandon Burrus Posted on January 10, 2021 12 min read

Love it or hate it, JavaScript is the most popular programming language according to the 2020 Stack Overflow survey. Over the years the language has developed a large ecosystem of libraries, frameworks, and tools around it. When you’re just getting started, it can feel overwhelming at first by the sheer amount of “things” you need to learn to become a proficient developer.

There are some key libraries that you can learn that will massively increase your value as a JavaScript developer. You’re very likely to find at least one (if not several) of these libraries in any given project you land in, further increasing the value of learning of them.

These libraries are going to be ordered by difficulty of learning, so it’s recommended to go one by one until you’ve got a good grasp on all of them. The goal of this article is to explain what these libraries do, as well as give you a basic introduction to them. We’ll start with the most ubiquitous of them all: Lodash.

Lodash

A virtual tool belt of utilities for the JavaScript developer, knowing just a few of the many functions that Lodash gives you can greatly increase your developer productivity.

Lodash itself is a “utility” library, meaning that’s only goal is to do exactly that: give you a wide set of little utilities that can help you accomplish mundane tasks such as partitioning an array, memoizing functions, or unescaping HTML entities in a string.

By no means do you need to memorize the complete list of all the Lodash functions. A simpler approach would be to look through their list of documented functions, noting useful or interesting ones as you go.

Over time, you’ll encounter the same problems over and over again, and can easily reach for Lodash to help you out!

The library itself started as a fork of the library Underscore, and the two share much in common. There’s a very good chance that the next company you work for will be using one or the other for just about every project.

Here are some useful/interesting lodash functions I’ve found myself reaching for time and time again:

  • partition() — Breaks an array into two separate arrays based on a predicate function in O(n) time.
  • compact() — Removes any kind of falsey value from a given array.
  • groupBy() — Groups a collection into a leaf plot-like object structure based on the given grouping function.
  • shuffle() — Scrambles the indices of an array into a random order.
  • sortBy() — Sorts an array by the given predicate input (this is more useful than the built-in array .sort() method as it returns a new array and can take in multiple, simpler inputs).
  • curry() or curryRight()— Curries a function, enabling partial application of function parameters.
  • debounce() — Debounces the given functions execution based on the defined wait time.
  • memoize() — Memoizes the given function, can also take a memoization resolver for more complex memoization when dealing with arrays or objects.
  • cloneDeep() — Deeply clones an object.
  • isNil() — Returns true if the given input is either null or undefined.
  • round() — Rounds a number to the given precision.
  • chain() — Wraps a value with a special Lodash wrapper object, allowing you to chain any Lodash function as a transformation (you can get the final transformation result by ending the chain with a call to .value()).
  • unescape() — Converts HTML entities into the actual unicode characters those entities represent.
  • get() — Safely do a deep traversal of an object based on the given path (if you’re working in TypeScript or using ES2020+, use Optional Chaining instead of using this).

Get started with Lodash here: Lodash Documentation.

Immutable.js

JavaScript has seen a trend using a more functional programming-style of development thanks to the widespread adoption of React in recent years. Part of functional programming is making use of immutable data structures. However many of the built-in functions for arrays are self-mutating, meaning they change the array in-place (such as arrays .sort() or .splice()).

Immutable.js seeks to solve this problem by providing powerful immutable data structures such as Lists and Maps. It makes converting between the immutable objects it provides and the native JS objects and arrays nice and easy. It also provides far more transformation and utility APIs for manipulating these structures than the native JavaScript provides for objects and arrays.

A quick overview of the core structures from Immutable.js:

List

A List are essentially the immutable version of an array: an ordered (indexed) sequenced collection of items.

We can create an empty list like so:

const emptyList = List();

Or we can create a list from an existing collection such as an array:

const myList = List([1, 2, 3]);

All of the transformation methods like filter() and map() exist on lists like we would expect for arrays:

const onlyEvents = List([1, 2, 3, 4, 5])
  .filter(n => n % 2 === 0);

const doubledNums = List([1, 2, 3, 4, 5])
  .map(n => n * 2);

But lists have other useful APIs such as interleaving or subset checking that make working with lists incredibly effortless.

The full documentation for Lists can be found here.

Map and OrderedMap

A Map is similar to an object, in that it’s an unordered collection of key-value (or KV) pairs (with the OrderedMap being the same, except with the key’s in a specific order and being sortable).

We can create an empty map just like we can an empty list:

const emptyMap = Map();

And we can of course create a map from an existing JS object:

const myMap = Map({ hello: 'world' });

The structure provides such basic manipulation such as retrieval, deletion, and merging:

const majorCities = Map({
  germany: 'Berlin',
  kenya: 'Nairobi',
  russia: 'Moscow',
  colorado: 'Denver',
  japan: 'Tokyo',
});

majorCities.get('germany');

majorCities.delete('berlin');

majorCities.merge({
  brazil: 'Rio de Janeiro',
  finlind: 'Helsinki',
  norway: 'Oslo',
});

The key thing to remember here is the above structure is immutable, so when we call a method like .delete() or .merge(), these methods return a new map object, instead of modifying the original map.

And just like lists, maps have all kinds of useful methods for performing things like flipping, taking, and even reducing.

The full documentation for Maps can be found here, and for OrderedMaps here.

Set and OrderedSet

Sets are collections of unique values (with the ordered version being exactly that: an ordered collection of unique values).

We can create sets the same way we can lists or maps:

const emptySet = Set();
const nums = Set([1, 1, 2, 2, 3, 3]);

If we were to log nums.size(), we would see 3. This is because the only unique values in the Set are 1, 2, and 3.

Set’s are useful when you need to deal with unique items. A trick you might see is creating an OrderedSet from an existing List to prune the list of any duplicate items.

The full documentation for Sets can be found here, and for OrderedSets here.

There’s more than just Lists, Maps, and Sets, Immutable.js has other data structures such as Records, Stacks, and Seqs to name a few.

Get started with Immutable.js here: Immutable.js Documentation.

RxJS

RxJS is all about streams of data and performing composable transformations on those streams. A more simplified way to think about this library is:

“Think of RxJS as Lodash for events.”— RxJS Documentation

RxJS makes working with complicated changes over time (asynchronous) much easier.

Observables

The core entity is the Observable. You can think of an Observable somewhat like an array of values, except the values can be separated by time. This is where we get the idea of a “stream” from, as in a stream of data or values over time.

Observables make working with (and reacting to) these streams nice and succinct. Take for example the from() function from RxJS, this takes in an array as a parameter and returns that array as an observable. Once we have an observable, we’ll typically want to subscribe to it.

const nums$ = from([1, 2, 3]);
nums$.subscribe(value => {
  console.log(value);
});

The dollar sign after nums$ is a convention JS devs like to use when working with RxJS and is just used to signify that the variable contains a reference to an observable.

In the code example we’re creating the observable nums$ from an array of numbers. Once we have that observable, we can subscribe to it to log out all of the values from that observable stream.

Lets see a more useful example:

const clicks$ = fromEvent(
  document.querySelector('#myBtn'),
  'click'
);
let clickCount = 0;
clicks$.subscribe(clickEvent => {
  clickCount++;
});

In this example we’re using the fromEvent() function from RxJS to create an observable from a DOM node event listener. This gives us a stream of events, which we can use to increment our click count.

But we’re using a very imperative-style of programming to handle that click count. Where RxJS really shines is in its ability to apply complex transformations to observables.

Operators

Operators in terms of RxJS are special functions that apply transformations to observables. Take our click counter example: instead of imperatively tracking the number of clicks in a variable, we’ll change this to use the scan() operator to transform each click event into an incrementing number.

So we’ll go from an observable of click events to an observable of (increasing) numbers.

const clicks$ = fromEvent(
  document.querySelector('#myBtn'),
  'click'
);

clicks$
  .pipe(
    scan(accumulator => accumulator + 1, 0)
  )
  .subscribe(clickCount => {
    console.log(clickCount);
  });

Operators are applied to observables using the .pipe() method. The scan() operator works similar to how .reduce() works: it takes a function and an initial accumulator value as arguments. The function gets the current value of the accumulator and the observable value as parameters, and needs to return the next new accumulator value.

In the above example, the scan function argument doesn’t care about the event, only the accumulator (this is why we’re ignoring that second parameter). The initial accumulator value is 0 and every time the button is clicked the operator transforms that event into an incrementing number.

Subscriptions

One important aspect of observables is the subscription object that gets returned when calling the .subscribe() method.

Observables can be either finite or infinite. When an observable is finite (such as our array example), the subscription will automatically be closed once the observable stream completes. However, for infinite observables (such as our click event observable) we need to be sure to explicitly unsubscribe the observable at some point, otherwise we’ll encounter memory leaks in our application.

This can be done by simply calling the .unsubscribe() method on the returned subscription object.

const clicks$ = fromEvent(
  document.querySelector('#myBtn'),
  'click'
);
const clickSubscription = clicks$.subscribe(e => {
  console.log(e);
});
clickSubscription.unsubscribe();

This is just the very tip of the iceberg when it comes to RxJS. The library has over one hundred operators readily available for you to use, and you can always create your own operators as well!

If you’re still not convinced that RxJS is an incredibly useful library, the article The Introduction to Reactive Programming you’ve been missing by André Staltz goes into much more detail about all the whys and hows of RxJS.

Get started with RxJS here: RxJS Documentation.

Ramda

Ramda does essentially the same thing that Lodash does (providing tons of really useful utility functions), but with one massive difference: Ramda utilities are all meant to be used in a Functional Programming-style.

If you’re coming to JavaScript from a language like Haskell, Elixir or OCaml, Ramda is going to be your best friend.

Functional programming is all about functions. Duh! One of the core fundamentals of functional programming is the use of composition.

Let’s look at the following pure function:

const double = n => n * 2;

Now say we want to take a number and quadruple it. We can of course just multiple the number by four, or we could call this double function on the number twice.

double(double(42));

We could abstract this quadrupling functionality into it’s own function like this:

const quadruple = n => double(double(n));

This is what composition is all about: taking existing functions, and composing them together to make bigger function. In the example, the quadruple function is composed of the double function.

Ramda can make this more readable by using the compose() function:

const quadruple = compose(double, double);

And the resulting quadruple function is the same.

All Ramda functions are designed to make working with composition as if you were working in an actual pure Functional language.

Every function from Ramda has two rules:

  1. Every function is curried.
  2. Every function defines the data a function operates on as the last argument.

For example, Ramda has a filter() function that works essentially the same as the native .filter() method:

const nums = [1, 2, 3];
const onlyEven = filter(n => n % 2 === 0, nums);

At first this looks odd, as we would normally expect that the nums array should be the first argument to filter(), not the filtering function.

But remember, filter() from ramda is curried, so we could actually do something like this:

const nums = [1, 2, 3];
const evenFilter = filter(n => n % 2 === 0);
const onlyEven = eventFilter(nums);

Combine data argument-last curried functions with composition, and you can define functional transformation functions such as this:

const transform = compose(
  add(2),
  multiply(2),
  subtract(5)
);transform(10); // 12

This example is contrived, but apply this technique to something more complex and we can see the real value that something like Ramda brings to the table.

Say I had the following data structure:

const records = [
  {
    title: 'Section B',
    entries: [5, 1, 2, 10, 7],
  },
  {
    title: 'Section A',
    entries: [4, 1, 2, 7]
  },
];

And the goal here is to do the following:

  • Sort each record object in the array by title in ascending order
  • Sort the nested entries in each record object by numeric descending order

Using Ramda utilities, we can create our own mapping util to satisfy these requirements that might look something like this:

const recordMapper = compose(
map(over(lensProp("entries"), sort(descend(identity)))),
sort(ascend(prop("title")))
);

Not that we’re playing code golf here, but imagine how many lines of code the same transformation function would’ve taken in an imperative style. Instead we’ve essentially compacted all of that logic down into three lines.

This example is fairly rigid as it only works with our given data structure. Typically you would break this util up into a few smaller functions first. That’s the end goal of Functional Programming via composition: to build bigger functions out of smaller ones!

If you’re looking to really dive deeper into using more Functional Programming-style approaches using Ramda, the brilliant article series Thinking in Ramda by Randy Coulman is a fantastic place to start!

It’s worth noting here that Ramda is the least common of the aforementioned libraries you’re likely to see on a project. However, if you still want to make use of a functional-style approach, Lodash has a submodule lodash/fp which is all the Lodash functions by built in a FP manner.

Get started with Ramda here: Ramda Docuemntation.

Conclusion

Learning these libraries will make you not only a better JavaScript developer, but just a better developer overall. This is because 3 out of the 4 libraries discussed not only give you a tool belt of incredibly useful functionality, but because they challenge the conventional imperative or object-oriented style of programming that most people first learn when picking up a programming language such as JS.

A fundamental aspect of being a good programmer is making sure you pick the right tool for the job. Thats exactly what these libraries are: a set of tools for you to use, just one of many approaches made available to you to solve any given problem. And at the end of the day, that’s what we as developers do: solve problems.