loading...

What happened to Immutable.JS? And how can we react?

alekseiberezkin profile image Aleksei Berezkin ・6 min read

Everyone who starts thinking about improving their JS code ends up studying tutorials about declarative style, functional programming, and immutable data structures. And the first and foremost immutable candidate to try is likely Immutable.JS. It's very popular lib with about 3M weekly downloads. Popular means good. Millions can't be wrong, can they?

A brief overview

Immutable.JS is a library implementing most useful persistent data structures. Each structure has methods to easily manipulate data.

import { List } from 'immutable'

// Find 3 most used letters
List.of('I', 'doubt', 'therefore', 'I', 'might', 'be')
    .flatMap(s => s)
    .groupBy(c => c.toLowerCase())
    .map(group => group.count())
    .sortBy(count => -count)
    .take(3)
    .toArray()

// Returns: [['e', 4], ['i', 3], ['t', 3]]

The library mirrors most of JS native structures like array, map, set, yet interoperates well with them. It supports ES6 iteration, ships Flow and TypeScript annotations, and is transpilable to ES5. So, diving in?

Let's first check a project pulse. Just in case

It's always a good idea checking the overall project liveness before using it. For our lib, concerns start right from the project header on npm:

Immutable.JS on npm

It's being a 4.0.0 release candidate for 2 years. That seems odd. What's with commits? That's the last one:

The last commit

It has some comments, for example:

Comment on last commit

The previous commit was almost a year ago, on Feb 14, 2019. There are also a lot of open issues and pending pull requests. This doesn't look like anything good.

One of the saddest places on GitHub

After browsing a bit, we finally see it:

Immutable.js is essentially unmaintained

There's a long conversation where the most active contributors ask of giving them at least the permission to manage issues. The creator and the only person with a write access first showed himself eager to grant an access to volunteers but then disappeared and is still inactive. What a sad story! 😭

What to do then?

That depends on relationships between you and the lib.

It's in my production!

Perhaps it's possible to live with it β€” however it's a good idea always keeping an eye on npm-audit. Right now Immutable.JS doesn't have known vulnerabilities.

If there's a bug blocking your work you may consider using the community fork or to create your own.

I wanted to try it but now...

Well, if you have options it's better to avoid using unsupported project. What is the possible replacement? Again, it depends. Try answering this question:

Q: Why do you want it on the first place?

A: I want to protect my data from accidental modification

There are some ways of doing that in JavaScript:

What to choose depends on your context. That's why libraries usually don't do anything here leaving the decision for you, and Immutable.JS is no exception. Therefore, you may not need it: just freeze any array or object, and get yourself safe.

A: I heard immutable structures are faster than arrays in a functional code

In the JS world it's true on carefully picked benchmarks. The main benchmark to prove this statement is concat (and its analogues: push, append etc) β€” the operation allows reusing parts of the source structure thus may cost O(log(n))O(log(n)) or even O(1)O(1) .

However it's hard to imagine how operations like map may be faster given you have to lift an array to that fancy data structure first. Surprise, there's no my-custom-data literal in JS! Surprise #2, List.of(array) costs O(n)O(n) πŸ˜‰ Surprise #3, JS builtins and most of libs work with native structures, so you'll always have to jump between arrays and custom lists, wasting valuable CPU on building and deconstructing hash map tries.

A: I just want my code to be concise, clear and side-effects-free

Good news: you don't need complex data structures for this! There are several ways of doing this in JS.

1. Higher-order functions of native array

For the moment, standard JS array has quite a few methods to help you: map, flatMap, filter, reduce etc. Just use them and don't modify input data in your functions.

2. Using external functions

Sometimes built-in methods are not enough: you may want extra convenience like grouping, zipping, splitting etc. The idea here is just to have separate functions which take an array as an argument. Ramda and Sanctuary are examples of libs containing collections of such functions. Most of functions have curried analogue:

import {
    chain, filter, groupBy, map,
    pipe, sortBy, take, toPairs
} from 'ramda'

pipe(
    chain((s: string) => [...s]),
    groupBy(c => c.toLowerCase()),
    toPairs,
    map(([c, {length}]) => [c, length] as const),
    sortBy(([_, length]) => -length),
    take(3),
)(['I', 'doubt', 'therefore', 'I', 'might', 'be'])

// Returns: [['e', 4], ['i', 3], ['t', 3]]

TypeScript note: because TS infers types top to bottom, an initial type has to be specified somewhere in the pipe beginning.

3. Stream-like wrappers

Unlike external functions, here you first create a wrapper which implements methods for data manipulation, then call these methods. Compared to external functions approach, it reads more β€œfluently”, top-to-bottom, left-to-right, which also helps TypeScript inferring types, and your editor to give reasonable suggestions.

This may look very like persistent structures approach, but it's completely different inside: wrappers are thin and lightweight, they are constructed in negligible O(1)O(1) time; they do not contain any data besides the reference on input. Yet, they usually don't produce intermediate arrays, so may save you some memory.

import { streamOf } from 'fluent-streams'

streamOf('I', 'doubt', 'therefore', 'I', 'might', 'be')
    .flatMap(s => s)
    .groupBy(c => c.toLowerCase())
    .map(([char, {length}]) => [char, length] as const)
    .sortOn(([_, length]) => -length)
    .take(3)
    .toArray()

// Returns: [['e', 4], ['i', 3], ['t', 3]]

Examples of libs implementing this:

Note: of those listed above only Sequency and Fluent Streams are ES6-iterables compatible.

4. Immer

Immer takes a completely different approach solving another problem; however the story would be incomplete without it. The lib allows having side-effect-free functions without limiting you to only non-mutating operations. It's especially useful in React + Redux setup; Redux Toolkit uses it by default. With the lib, you may write like:

import produce from 'immer'

const iKnow = ['JavaScript', 'TypeScript', 'Java']

// Creates a new array not modifying original
const iLike = produce(
    iKnow,
    draft => { draft.push('Kotlin') },
)

Besides, Immer can freeze produced objects giving you immutability guarantees.

So finally

Going back to Immutable.JS. Seriously, having it abandoned is the severe loss for the whole webdev community. I wholeheartedly wish its creator, Lee Byron, to find some time and give folks willing to contribute a chance! Hope one day we'll see 4.0.0 stable with shiny new features and all major issues fixed.

Posted on by:

alekseiberezkin profile

Aleksei Berezkin

@alekseiberezkin

Fullstack dev: Java, JS, TS, React

Discussion

pic
Editor guide
 

This comment got way too long, sorry.

I'm using persistent data strucutres in node and gain both performance benefits and purity. However, I'd say you don't necessarily need immutable.js:

Single Linked Lists

Single linked lists have the form of a pair tuple [head, tail]. While they are little familiar in Javascript they are inherently persistent and thus have several benefits:

  • unshift/shift for free
  • list concatenation as efficient as with mutable arrays

Here is an example:

list append

Objects as Trees with Sharing

If you use objects as trees you can merely update the path to a node while the rest of the tree structure is shared. This is a very efficient copy mechanism. You can find an implementation in my lib: github.com/kongware/scriptum/blob/...

Hash Array Mapped Tries

If you really need persistent (ordered) maps/sets or arrays you can implement a HAMT yourself. There is a basic implementation in my lib. It is really not that hard.

 

Thanks for sharing links. While linked lists definitely have their usage, we must keep in mind that access by index costs O(n)O(n) for them. Yet, because each item produces additional object(s), memory fragmentation increases.

Ordered map is indeed a problem in JS, and devs should either implement it, or use some lib, or just order and array of [key, value] tuples. Which is best? It depends πŸ™‚

 

You're right. However, I think index based access is overrated, because you don't need it that often.

Btw. I don't think immer.js is a good solution, because it doesn't seem to compose.

 

Yeah, as you said there at the end of the post, for Redux we specifically recommend:

 

Thanks for your reply! It's always nice to have an official statement under the post πŸ˜€

 

I think it is worth a mention of the records and tuples proposal which made it to stage 2 and which brings native immutable data structures to js.
github.com/tc39/proposal-record-tuple

 

Was about to mention that πŸ˜‰

Check my article as well
dev.to/sebastienlorber/records-tup...

 

Another one to try in case if you want lazy stream-like operations: Undercut. It's based on Iterators and you can run your own operations.