Last active
November 8, 2016 04:02
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(() => { | |
/**************************** | |
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