Skip to content

Instantly share code, notes, and snippets.

@trobertsonsf
Last active November 8, 2016 04:02
Show Gist options
  • Save trobertsonsf/18e2a2906e3221b6c085 to your computer and use it in GitHub Desktop.
Save trobertsonsf/18e2a2906e3221b6c085 to your computer and use it in GitHub Desktop.
Immutable.js can have a bit of a steep learning curve, this is trying to show you how to do basic things and why you should care.
(() => {
/****************************
Before you start, you should watch this: https://www.youtube.com/watch?v=I7IdS-PbEgI
*****************************/
/****************************
NOTE: You need Immutable to be defined globally for this to work
I go to: https://facebook.github.io/immutable-js/
Open dev tools and paste it into the console
*****************************/
/****************************
Also note, I wrote this on a plane back from React Conf, with mostly
a fried brain, so it turned into this weird conversationaly style.
Honestly, this is how most of my conversations / training for Immutalbe go
so I just rolled with it.
*****************************/
if(!window.Immutable) {
console.error('Immutable is not defined! Go to https://facebook.github.io/immutable-js/ and run in the console there');
return;
}
//Make sure, for {} and [] that a and b are the same reference.
const assertReferenceEqual = function(a, b) {
if(a === b) {
console.log('they are ===')
} else {
console.error('NOT ===')
}
}
//Assume that the values in a and b are equal.
const assertValueEqual = function(a, b) {
if(JSON.stringify(a) === JSON.stringify(b)) {
console.log('they are JSON.stringify ===')
}else{
console.error('NOT JSON.stringify ===')
}
}
const items = {
a: {
name: 'Lee'
},
b : {
name: 'Tim'
}
};
const otherItems = items;
assertReferenceEqual(otherItems, items); //yup
//To check if the values of a object are the same is a tricky problem.
//If you want to look at what it takes to actually implement this for real
//look at the underscore annotated source: http://underscorejs.org/docs/underscore.html#section-113
//it's 50+ lines of clever code.
//For this example, we are just going to manually compare each of the keys
console.log('a.name should be true:', otherItems.a.name === items.a.name);
console.log('b.name should be true:', otherItems.b.name === items.b.name);
otherItems.a.name = 'Bob';
assertReferenceEqual(otherItems, items); // yup still same ref
console.log('should be true:', otherItems.a.name === items.a.name, 'because we changed both values to', otherItems.a.name, items.a.name);
console.log('b.name should be true:', otherItems.b.name === items.b.name);
//This is a little tick to "clone" an object. We first stringify the existing object so all of it's values are serialzed and then
//JSON.parse creates a brand new object that is populated with the same values. We could have also just copy and pasted the same values
/*
const copyOfItems = {
a: {
name: 'Lee'
},
b : {
name: 'Tim'
}
}
*/
const copyOfItems = JSON.parse(JSON.stringify(items));
assertReferenceEqual(copyOfItems, items); // nope, new instances
console.log('a.name should be true:', copyOfItems.a.name === items.a.name); // yup same values
console.log('b.name should be true:', copyOfItems.b.name === items.b.name); // yup same values
//Let's make the same update as above.
copyOfItems.a.name = 'Harry';
//Note that items.a.name is 'Bob' because we changed it up above
console.log('a.name should be false:', copyOfItems.a.name === items.a.name); // false, we updated 2 different instances
console.log('b.name should be true:', copyOfItems.b.name === items.b.name); // yup same values
//Comparing the value equality of 2 objects is really hard, I'd never recommend trying to do it yourself.
//Even if you do use underscore/lodash, their (brilliant) implementations are going to have to potentially traverse 2 huge objects making a ton of value comaprisions
//which is also super slow. There's got to be a better way. This is the basic premise of Immutable.js
//We get a guarentee that if the values are the same we will have the same reference, meaning we can
//use a simple === check for complex types. This is done by creating new instances of objects that are modified
//(in a very efficient way). This means that if you have a huge object and only modify a single field, only the item affected
//and its partents will get a new instance.
//A more complex object:
//Note the indexes don't correspond to the hashtag data in any meaningful way, they're just for show
const bigItems = {
data: {
'abc': {
id: 'abc',
author: {
name: 'Tim',
twitter: '@rtimothy'
},
displayText: {
text: 'A tweet about #toast and #coffee',
containsHtml: false
},
hashTags: [
{
text: 'toast',
index: {
start: 48,
end: 55
}
},
{
text: 'coffee',
index: {
start: 65,
end: 71
}
}
],
uiState: {
isSelected: false
}
},
'def': {
id: 'def',
author: {
name: 'Lee',
twitter: '@leeb?'
},
displayText: {
text: 'some text and stuff #graphql',
containsHtml: false
},
hashTags: [
{
text: 'graphql',
index: {
start: 48,
end: 55
}
}
],
uiState: {
isSelected: false
}
}
},
metadata: {
moreItems: '/foo/api/items?startIndex=10&pageSize=10&direction=forward',
previousItems: '/foo/api/items?startIndex=10&pageSize=10&direction=backward',
}
}
//This is much more reprsentative of the type and layout of data you'd have in a real React based app.
// Now let's throw it into immutable.
var immutableItems = Immutable.fromJS(bigItems);
//Let's look at what we have
console.log(immutableItems);
//WTF?? what's that?
/*
Object {size: 2, _root: ArrayMapNode, __ownerID: undefined, __hash: undefined, __altered: false}
__altered: false
__hash: undefined
__ownerID: undefined
_root: ArrayMapNode
length: (...)
size: 2
__proto__: Map
*/
//Yeah, that's not very useful, but it does tell us a couple of useful things: it's a Map and it has size 2
// Map is {} and List is [] as a starting point
//There's got to be a better way?
console.log(immutableItems.toJS())
//Ah, that's much better.
//.toJS is your escape hatch. There's really only 2 places to use .toJS in my opinion 1) in the console when you are debugging or logging
// 2) when you are about to go over the network, you take your Immtable representation and convert to JS, then JSON (by your ajax lib)
//if you are using .toJS elsewhere in your code, because you can't find a way to do something with other Immutable functions
//this is a code smell.
//Ok, so if we can't .toJS() how do we actually make use of this stuff?
//We use .get()
console.log(immutableItems.get('data'))
console.log(immutableItems.get('data').toJS())
//How do I go deeper?
console.log(immutableItems.get('data').get('abc').get('author').get('name'))
//Wait? no .toJS there? nope, it's just a value, a string in this case.
//But that's kinda ugly
//That's what .getIn is for
console.log(immutableItems.getIn(['data', 'abc', 'author', 'name']))
//What if something doesn't exist?
console.log(immutableItems.getIn(['data', 'BAD_KEY', 'author', 'name']))
//hmm, ok, at least it didn't blow up, but undefined kinda sucks.
//welp, there's a second paramater for .getIn and .get that is the not found value
console.log(immutableItems.getIn(['data', 'BAD_KEY', 'author', 'name'], ''))
//Ah, that's cool, now I don't need a million null checks!
//So how do I update stuff?
//with .set / .update or .setIn / .updateIn
console.log(immutableItems.set('data', 'something dumb').toJS());
//ah, ok
//well, not quite... lets log again
console.log(immutableItems.toJS());
//Huh? I thought we just updated that??
//Well you did, but you threw away the result. Remember how we said that when you modify an immutable structure
//you get a new copy of what you modified plus all of it's parents to the root?
//yeah, i guess?
//Well, you did a modification, and didn't hold on to the new instance. This is a super common mistake
//that people new to immutable make.
const dumbThing = immutableItems.set('data', 'something dumb');
assertReferenceEqual(dumbThing, immutableItems); // nope, we modified it so it's a differnt instance;
//Watch this
assertReferenceEqual(dumbThing.get('metadata'), immutableItems.get('metadata')); // they are ===
//Wait.... wtf?
//Yeah, this is the awesome part, our root Map has 2 nodes, data and metadata. We updated data, which meant its parent (the root)
//got updated but we didn't touch metadata so they both share the same reference. That means we can know that 2 references to metadata
//have the save value just by doing === (instead of something like deepEqual)
//I still don't quite get it...
//Cool, let's do another example and this time we can show you how it'll help in React
//pretend we have a React component that somehow shows all the data in our items and has a statement like this:
// {item.uiState.isSelected ? <SelectedIcon /> : null}
const abc = immutableItems.getIn(['data', 'abc']); //You'll see in a minute
const def = immutableItems.getIn(['data', 'def']); //You'll see in a minute
const updatedItems = immutableItems.setIn(['data', 'abc', 'uiState', 'isSelected'], true);
console.log(updatedItems.toJS()); //it's updated,
assertReferenceEqual(abc, updatedItems.getIn(['data', 'abc'])); //nope, not === like we expect
assertReferenceEqual(def, updatedItems.getIn(['data', 'def'])); //yup! ===
//pretend we now pass updatedItems into our React view heirarchy like Redux would.
//when we change some data, specifically the selected state of an item
//we want a new Render to happen and for our view to update. React will handle this for us, they're super smart and will
//see that only the view associated with item 'abc' will need to actually change the DOM. The problem is that it's also
//going to go through the whole vdom diff / reconsiliation process only to do all that work to realize it doesn't need to
//update 'def'. We can do better.
//React has a lifecycle hook called `shouldComponentUpdate` that will save us. It will be passed both
// the next state and props. We can then decide if based on those values we should return `true` to let React re-render our component
// or return `false` and short curcuit the render loop for this instances because the data hasn't changed.
//hmmm, what would that look like?
//Welp. you could write your own for such a tiny component:
// shouldComponentUpdate(nextProps, nextState) {
// return nextProps.isSelected !== this.props.isSelected;
// }
//Cool, guess we are done here.
//Not so fast. This would be a nightmare for any complex components and would do nothing
//but cause bugs.
//So what do I do?
//Use reacct-immutable-render-mixin https://github.com/jurassix/react-immutable-render-mixin
//dood. I have Twitter, Mixins are BAD.
//Yeah, kinda, but it's really just a leftover name, there are a number of ways to use the functionality
//without it being a mixin.
// shouldComponentUpdate(nextProps, nextState) {
// return !shallowEqualImmutable(this.props, nextProps) || !shallowEqualImmutable(this.state, nextState);
// }
//So let's trace this again. We had immutableItems with isSelected set to false for both items.
//React rendered that and it's beautiful. Now something happened (like the user clicked on item 'abc').
//So we update isSelected field for 'abc'. We then pass in our new `updatedItems` data into React and it
//flows down to all our components via props. For the <Item> instance that's rendering the data for 'abc',
//we are going to have a new reference so `shouldComponentUpdate` will return true and React will display the
// <SelectedIcon /> component for us. For the <Item> instance that's rendering item 'def' `shouldComponentUpdate` will
//return `false` because we have the same reference and we son't need to go through the whole vdom diff/reconcile phase.
//Ok, so what else about immutable?
//It has a lot of the common functional operations you'd expect like .map, .filter, .reduce etc.
//Show me.
//ok. if you don't know what .map and .fitler are, you should go read about them elsewhere.
//Here's a .map
const hashTags = immutableItems.getIn(['data', 'abc', 'hashTags']);
const displayTags = hashTags.map((ht) => {
return '#' + ht.get('text');
});
console.log(displayTags.toJS());
//And a .fitler
const tagsOver5Chars = hashTags.filter((ht) => {
return ht.get('text').length > 5;
});
console.log(tagsOver5Chars.toJS());
//What else can you tell me?
//Updates can be kind of hard. Namely, you can shove in a plain JS object into the middle of an Immutable Map / List
//Huh?
//So far I have only shown you .fromJS which does a deepConversion of all the things to their corresponding
//Immutable types and .set where we are just updaing a simple value. But we might need to do some more fancy stuff.
//Show me.
const newHashTag = {
text: 'tatertot',
index: {
start: 99,
end: 107
}
};
//re-using hashTags from above
const updatedHashTags = hashTags.push(newHashTag);
console.log(updatedHashTags.get(0)); //.get(0) is like array[0]. This returns a Map =]
console.log(updatedHashTags.get(2)); // This returns a JS Object =[
//Huh???
//I told you that you could put regular JS inside Immtuable Maps / Lists... so you have to be careful.
//How do I fix it?
//re-using the starting hashtags because we still have the old, un-modified instance. this is cool. please clap.
const updatedHashTagsAllImmtuables = hashTags.push(Immutable.fromJS(newHashTag));
console.log(updatedHashTagsAllImmtuables.get(2)); // A Map =]
//Note, you can crete a Map like this:
const m = Immutable.Map({a: 1, b: 2});
console.log(m)
console.log(m.get('a'))
//But it won't convert all the way down....
const m2 = Immutable.Map(bigItems.data.abc);
console.log(m2) //cool, a Map
console.log(m2.get('hashTags')); //crap, a JS object
//Honestly, I just use .fromJS() for all this. There's also .merge / .mergeIn we'll talk about real quick.
//What is a merge? It's like a combo of set/update and fromJS
//cool.
//Let's say we have some more ui state in our item
const moreItems = {
data: {
'abc' : {
uiState: {
error: false,
errorMessage: null,
isSelected: false,
isLoaded: false
}
}
}
};
//Make it a Map
const itemMap = Immutable.fromJS(moreItems);
//Pretend we make a request to load more data or something for item `abc` and it failed
const updatedState = {
isLoaded: true,
error: true,
errorMessage: "You are not authorized to view this item"
};
//That's the new state we want, there are a couple ways to handle this.
//using a bunch of .setIn calls.
const updatedItemMap =
itemMap.setIn(['data', 'abc', 'uiState', 'isLoaded'], updatedState.isLoaded)
.setIn(['data', 'abc', 'uiState', 'error'], updatedState.error)
.setIn(['data', 'abc', 'uiState', 'errorMessage'], updatedState.errorMessage)
console.log(updatedItemMap.toJS()); //that worked.
//Yes, that works, but it's also super slow, you end up going through that overhead of creating
//a bunch of new versions of the Map that we just slow away.
//oh yeah, that whole create a new instance thing... what do we do about it?
//You could still use .setIn and wrap it in a `withMutations` call.
//What's that?
//It's a way to wrap all these calls to make them more efficient.
const withMutationsMap = itemMap.withMutations((im) => {
return im.setIn(['data', 'abc', 'uiState', 'isLoaded'], updatedState.isLoaded)
.setIn(['data', 'abc', 'uiState', 'error'], updatedState.error)
.setIn(['data', 'abc', 'uiState', 'errorMessage'], updatedState.errorMessage);
});
console.log(withMutationsMap.toJS()); //cool.
//That kinda ugly though?
//Yeah, for .set it's a bit ugly, there are other times when you need to do a bunch of operations
//and it comes in handy. There's another way to make this a lot cleaner.
//Why didn't you just tell me that then?
//I'm educating you, be more appreciative.
//Whatever.
//Let's use .merge
const mergedMap = itemMap.mergeIn(['data', 'abc', 'uiState'], updatedState);
console.log(mergedMap.toJS()); //cool!
//One more thing about merge:
const nestedUpdatedState = Object.assign({}, updatedState, {
nested: { obj: [1,2,3] }
});
const anotherMergedMap = itemMap.mergeIn(['data', 'abc', 'uiState'], nestedUpdatedState);
console.log(anotherMergedMap.toJS()); //that looks good!
console.log(anotherMergedMap.getIn(['data', 'abc', 'uiState', 'nested', 'obj']));
//I'm not going to go into all the detaisl around .merge and .mergeDeep, but know there
//are some subtleties around how things are converted and merged, you should read the docs.
//Dood! I tried reading the docs, they are crazy.
//I don't think crazy is fair. They are more of a language reference than a programming guide. \
//WTF does that mean?
//It's a bit hard to get up to speed, that's why I created this silly walkthough. But once you understand
//the basics, the docs are really easy to follow. Also if you're familiar with Java at all they will make a lot
//of sense becuase they are similar to how Java uses generics.
//LOL, java. that's stupid.
//ugh, you're killing me. Let me just give you one example using Map.
//Maps are basically, well, a mapping from a key to a value. That key can really be anything, but in general
//you want to have the types of things you use for Keys (K) and Values (V) be the same type of things.
//The most common is going to be a Map() of String Keys to other Map or List Values.
//The generic way to write that is Map()<K, V>. You can think of the values we creaed above with items as a
//Map<String, Map> That means we have sting keys (at the top level), with values that is a Map
//As we go down the structure, if we did a getIn for hashTags we'd get a Map<String, List>. Futhermore that
//List would be a List<Map> (a List of Maps).
//So when you read the docs, and you see K and V all over the place, they are just trying to generically
//express that Maps can hold types for Keys and Values,
//When you look at the docs for .get: get(key: K, notSetValue?: V): V
//It's saying, that give a type K and an optional notSetValue (remember that from earlier?) it will return a type V.
//You have to do the translation to the types relevant to your instance in your brain.
//ah, that helps I think. What else?
//It takes a little bit of time to get up to speed, but it's TOTALLY worth it. I see a lot of people struggle
//because they are trying to learn React and Redux and Immutable at the same time. I recoomend just trying to do
//basic data manipulation with Immutable in the console to get comfortable with Immutable.
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment