Working with Immutable Maps or Lists is one thing, but how do you work with complex objects, such as deeply nested Maps in a List, or a List in a deeply nested Map? This tutorial shows you how to actually use Immutable in real world situations.

Add List items to a List nested deeply within a Map

Suppose you have a List that’s deeply nested within a Map. This can happen with complex JSON data retrieved from a server, or in a Redux store, for example.

You then receive more data that needs to be appended to your List within its Map. How would you use the Immutable methods we’ve learned to achieve this?

Before I discuss the answer, see if you can do it yourself in the Live Editor below (note: there are many different ways it can be done!)

The Problem


// Our existing data
const state = Immutable.fromJS({ 
	actors: {
		name: 'Scarlett Johansson'
	},
	heroes: {
    	heroList: [{
			heroName: 'blackWidow',
			realName: 'Natasha Romanoff'
		}]
	}
});

// New data to append to the existing heroList
const heroList = Immutable.fromJS([{
  heroName: 'ironMan',
  realName: 'Tony Stark'
}, {
  heroName: 'captainAmerica',
  realName: 'Steve Rogers'
}]);

// We want the following output:
// {"actors":{"name":"Scarlett Johansson"},"heroes" [{"heroName":"blackWidow","realName":"Natasha Romanoff"},{"heroName":"ironMan","realName":"Tony Stark"},{"heroName":"captainAmerica","realName":"Steve Rogers"}]}


The Solution

Here’s how I did it:


// Our existing data
const state = Immutable.fromJS({ 
	actors: {
		name: 'Scarlett Johansson'
	},
	heroes: {
    	heroList: [{
			heroName: 'blackWidow',
			realName: 'Natasha Romanoff'
		}]
	}
});

// New data to append to the existing heroList
const heroList = Immutable.fromJS([{
  heroName: 'ironMan',
  realName: 'Tony Stark'
}, {
  heroName: 'captainAmerica',
  realName: 'Steve Rogers'
}]);

// Here's the magic!
state.setIn(['heroes'], state.getIn(['heroes', 'heroList']).concat(heroList));

So how does this work?

We can’t use any of the (many many!) merge functions, as we want to concatenate two Lists together (heroList and state.heroes.heroList), not merge them (recall form the tutorial on how to merge Immutable.js Lists that a List.merge() will overwrite the original List’s items, not append the merging List to it).

So we need to use List.concat() instead. However, the List that we want to concatenate onto is nested deeply within the state Map, and there is no List.concatIn() method to traverse down this deeply nested object.

So the answer is to use Map’s setIn method on the state Map to traverse down the hierarchy to the heroList List, and then use List’s concat method to add the new values to our List, like so:

state.setIn(['heroes'], state.getIn(['heroes', 'heroList']).concat(heroList));

Breaking this down:

  • state.setIn(['heroes']... – traverses down to the value of the heroes property (i.e. the heroList List)
  • state.getIn(['heroes', 'heroList']).concat(heroList)); – replaces the entire heroList with this newly concatenated List (which comprises the original List values (courtesy of getIn()) and the new heroList values concatenated onto the end (via concat(), obviously!)

Change a Map nested deeply within a List of Maps

Add new key/values to a Map nested deeply within a List

Suppose your data comprises a List of Maps, with each Map containing its own deeply nested Map. How do you add new key/values to one of the deeply nested Maps?

Like this:


// Setup our List of deeply-nested Maps
const avengersMap = Immutable.fromJS([{
  hero1:  {
    ironMan: {
      realName: 'Tony Stark'
    }
  },
  hero2: {
    warMachine: {
      realName: 'James Rhodes'
    }
  }
}, {
  hero1: {
    captainAmerica: {
      realName: 'Steve Rogers'
    }
  }
}]);

// Give ironMan a partner
const newAvengers = Immutable.fromJS({
   partner: 'Pepper Potts'
});

avengersMap.mergeDeepIn([0, 'hero1', 'ironMan'], newAvengers);

In this example, we want to add the new key/value { partner: Pepper Potts } to the Map ironMan, but this Map is nested deeply within another Map (hero1), which itself is a List item.

To achieve this, we need to think of our new key/value pair as a Map, and then merge it into the ironMan Map. Once we think in terms of Maps, we can use the standard Map.mergeDeepIn() function to traverse down our complicated object hierarchy.

Immutable’s mergeDeepIn() and mergeIn() methods work with any Immutable Iterable object, letting you traverse down a List and a Map in the same call.

Note how Map.mergeDeepIn() works with both List indices and Map keys to traverse down the hierarchy.

Immutable will let you traverse down a List and a Map in the same call to mergeDeepIn(). Using numbers in the key path will let you traverse across Lists according to index, while key names will let you traverse down Maps, and both can freely be mixed in the same key path.


Change the value of a key in a Map that’s deeply nested within a List of Maps

Just as mergeDeepIn() will work with both Lists and Maps, so too will setIn(), or any of the other xIn() methods.

In this example, we’ll change the value of ironMan’s name by first specifying the List index we want to traverse to, and then the key name we ultimately want to set:


// Setup our List of Maps
const avengers = Immutable.fromJS([{
       heroName: 'ironMan',
       realName: 'Tony Stark'
   }, {
       heroName: 'captainAmerica',
       realName: 'Steve Rogers'
   },  {
       heroName: 'blackWidow',
       realName: 'Natasha Romanov'
}]);

// Change ironMan's name to manOfIron
avengers.setIn([0, 'heroName'], 'manOfIron');


Set multiple values in a Map in one go

In a deeply nested Map, you could have many nested child Maps that exist at different levels of the overall Map hierarchy. Sometimes (such as when new data arrives from a server), you’ll need to change two or more of these child Maps.

But how would you do this if each child Map is located at a different level of the root Map?

The Problem: change values in two nested Maps at different levels of a Map hierarchy

For this example, here’s our root Map, (represented as a big, hierarchical Map called avengers), with the newData that’s just arrived immediately below it.


// Setup our root Map
const avengers = Immutable.fromJS({
  heroes: {
    0: {
      heroName: 'ironMan',
      realName: 'Tony Stark'
    },
    1:  {
      heroName: 'captainAmerica',
      realName: 'Unknown'
    }
  },
  isAssembled: false
});

// Here's the newData, which should be used to update our Avengers Map
const newData = { realName: 'Steve Rogers' };

But there’s more. For this example, as well as updating our Avengers Map so that captainAmerica gets a realName, we also need to set the value of isAssembled to true at the same time.

In other words, we need the new value of our Avengers Map to be as follows:


const updatedAvengers = Immutable.fromJS({"heroes":{"0":{"heroName":"ironMan","realName":"Tony Stark"},"1":{"heroName":"captainAmerica","realName":"Steve Rogers"}},"isAssembled":true});


// Expected Output:
updatedAvengers

Both realName and isAssembled exist at different levels of our avengers Map, so how do we do that?

And can it be done in just one line of Immutable magicalness? Give it a go and see what you can come up with.

The Solution

Here’s how I did it (and yes, in one line!):


// Setup our Store
const avengers = Immutable.fromJS({
  heroes: {
    0: {
      heroName: 'ironMan',
      realName: 'Tony Stark'
    },
    1:  {
      heroName: 'captainAmerica',
      realName: 'Unknown'
    }
  },
  isAssembled: false
});


const newData = { realName: 'Steve Rogers' };

avengers.mergeDeepIn([], { heroes: { 1: newData }, isAssembled: true });

The key to the solution is to go high enough up the hierarchy that you cover all the nested Maps that you need update. You can change each nested Map individually if you want (e.g. first update heroes[1], then update isAssembled), but if you go up to a level in the hierarchy immediately above both nested Maps, you can alter them both with one call to mergeDeepIn().

Remember, mergeDeepIn() leaves intact those values in the Map being operated on whose keys don’t exist in the data being merged in

This method works because mergeDeepIn() will leave intact those values in the Map being operated on (i.e. avengers) whose keys don’t appear in the data you’re merging in (i.e. newData).


Advanced Immutable Recipes

We’ll finish this series with a couple of fancy recipes that will help you see the power of Immutable in ways you perhaps wouldn’t have expected.

Remove duplicates from an Immutable List()


const fixedAvengers = Immutable.fromJS([{
    id: 1,
    heroName: 'captainAmerica',
    realName: 'Steve Rogers'
}, {
    id: 2,
    heroName: 'blackWidow',
    realName: 'Natasha Romanov'
}, {
    id: 2,
    heroName: 'blackWidow',
    realName: 'Natasha Romanov'
}, {
    id: 3,
    heroName: 'theHulk',
    realName: 'Bruce Banner'
}]);


fixedAvengers.toSet().toList();



Create a histogram using groupBy

Immutable not only lets you merge and set data, it will also help you sort, group and filter it. Here’s how you can use it to create a histogram function, grouping data by frequency (i.e. how often it appears) within a set range (in this case, 10, 20, 30, 40 and 50.)

The result of this example will be a List of values, representing how many numbers appear from 0 – 10, 11 – 20, 21 – 30, etc.


const list = Immutable.List([1, 3, 5, 6, 8, 13, 22, 24, 25, 27, 28, 30, 31, 34, 40]);

const periods = Immutable.fromJS([10, 20, 30, 40, 50]);


const histogram = periods.reduce(function (hist, period) {
 	const groupedList = hist.nextList.groupBy((item) => item < period);

  	hist.frequencies = hist.frequencies.push(groupedList.get(true).size);
	hist.nextList = groupedList.get(false);

  	return hist;
}, { frequencies: Immutable.List.of(), nextList: list });

histogram;

Summary

This has been a much bigger set of articles than I expected. Initially I just planned on writing a series of HowTos (pretty much, just this post!), but I soon realised the official Immutable docs were so bad, I needed to write a whole series of examples first for the most-used Immutable objects (Lists, Maps and Sets).

Had I not needed Immutable for my work, and if it were not such a useful (and performant) library, I wouldn't have bothered, but I'm extremely glad I did.

If you're still feeling confused, or want me to add any more tutorials on Immutable, just let me know on Twitter.

Comments
  • Harvey Specter
    Posted at 3:45 am Dec 27, 2016
    Manish
    Reply
    Author

    Thanks much better than the docs!

  • Harvey Specter
    Posted at 12:22 pm Jul 30, 2017
    Dani
    Reply
    Author

    Thank you so much for this series Mike. I was about to give up on Immutable.js, but your series of articles was so helpful in getting started that now (a few weeks later) I’m in the middle of using it in production code!

    • Harvey Specter
      Posted at 1:33 pm Jul 30, 2017
      Mike Evans
      Reply
      Author

      Really pleased to hear that Dani 🙂

  • Harvey Specter
    Posted at 2:28 pm Aug 4, 2017
    Nathan
    Reply
    Author

    In the first solution, you can be even more terse and more efficient by using `updateIn`.

    Instead of the `get` & `set` pattern:
    state.setIn(['heroes'], state.getIn(['heroes', 'heroList']).concat(heroList));

    The more efficient `updateIn`:
    state.updateIn(['heroes', 'heroList'], (heroList) => heroList.concat(heroList));

    To my understanding `updateIn` does an update in place on the value, where as the `get` & `set` pattern will manually replace the entire value, although it will resolve the same, it takes a slight bit more effort.

    Docs: https://facebook.github.io/immutable-js/docs/#/List/updateIn

    • Harvey Specter
      Posted at 2:31 pm Aug 4, 2017
      Nathan
      Reply
      Author

      In addition, it is advisable to use just regular `.set` instead of `.setIn` when only going one key deep. `setIn` will iterate through the array which takes a bit more time for a single key over just a `set`.

      state.setIn(['heroes'], state.getIn(['heroes', 'heroList']).concat(heroList));
      becomes
      state.set('heroes', state.getIn(['heroes', 'heroList']).concat(heroList));

  • Leave a Reply to Dani
    Cancel Reply

    You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>