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 theheroes
property (i.e. theheroList
List)state.getIn(['heroes', 'heroList']).concat(heroList));
– replaces the entireheroList
with this newly concatenated List (which comprises the original List values (courtesy ofgetIn()
) and the newheroList
values concatenated onto the end (viaconcat()
, 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.
All Immutable.js Tutorials in this Series
This is just one tutorial in this in-depth series of Immutable.js tutorials. Here are the others, which all contain a wealth of information and examples (and all in JavaScript!).
Introduction
Lists
- The Foolproof Guide to Creating Lists
- Get, Set and Delete data from an Immutable List
- Merging Lists Finally Explained
Maps
- Every Way to Create an Immutable.js Map
- Get, Set, Delete and Update data from an Immutable Map
- 6 Ways to Merge Maps in Immutable.js
Manish
Thanks much better than the docs!
Dani
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!
Mike Evans
Really pleased to hear that Dani 🙂
Nathan
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
Nathan
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));