Immer is an incredible new library for JavaScript immutability. In previously libraries like Immutable.js it required whole new methods of operating on your data.
This was great but required complicated adapaters and converting back and forth between JSON and Immutable in order to work with other libraries if needed.
Immer simplifies this and you use data and JavaScript objects as your normally would. This means when you need performance and need to know when something changes you can use a triple equal strict equality check and prove that something has indeed changed or not changed.
Your shouldComponentUpdate
calls are no longer require shallow or deep equals to traverse all the data and compare.
In latest JavaScript many developers depend on the spread operator ...
to do immutability. For example you can spread the previous object in and override specific keys, or add in new keys. This will use Object.assign
under the hood and return a new object.
https://gist.github.com/b94526b7fd91dfe554261b3871a1de0a
Our newObject
will now be a completely different object so any strict equality checking (prevObject === newObject
) will be false. So it is indeed creating a new object. The name will no longer just be Jason
but will be Jason Brown
and because we didn't do anything with the id
key it stays the same.
This applies to React in that if you have a nested object on state
you need to update and spread in the previous object because React will only merge state
at the base level of keys.
Lets look at an example. Say we have 2 nested counters but we only want to update one and not mess with the other.
https://gist.github.com/3e7650af976cd5fa51c2d839652d6efa
Next in our componentDidMount
we'll set up an interval and update our nested counter. However we want to preserve the otherCounter
value so we need to use the spread operator to bring over the previous nested state.
https://gist.github.com/81f267084ca61e37c97b532510ee0204
This is an all too common scenario in React, and if your data is really nested it adds complexity when you need to spread more than one level deep.
Immer allows for us to still use the mutations (directly modifying a value) but without actually worrying about managing the number of spreads, or even what data was touched and needs to be immutable.
Lets setup a scenario where you are passing in a value to increase a counter by and additionally have a user object that isn't going to be touched.
Here we render our app and pass in the increment value.
https://gist.github.com/50dceb36e3be18a7387e8920b9cf9475
https://gist.github.com/9e269dd720c7dd4b3b6183a01a6e5d54
We setup our app just like before now with a user object and a nested counter.
We'll import immer
and name the default import produce
. As in when given the current state, it'll help us produce the next state.
https://gist.github.com/9852f7a455294a34490e969d32b10061
Next we'll create a function called counter
that takes state and props so we can read the current count and then update with the next count based upon the requested increaseCount
prop.
https://gist.github.com/fa36577702a428f2ea30fb15ef9655f7
The produce method of Immer takes a state
as it's first argument, and a function to mutate your data for the next state as it's second argument.
https://gist.github.com/0c79dcd24c33b0cc2516204e75414b05
Now if we put it all together. We can create a counter function that takes some state and some props and calls the produce function. We then mutate the draft
of what the next state should look like and the Immer produce function will create a new immutable state for us.
https://gist.github.com/a2c2a0dd4184aac3f5c2eca9c3d71392
Our updated interval function might look something like this.
https://gist.github.com/f1b6cfa01e5baf56821487aab082dafb
However we only touched the count
and counter
, what happened to our user
object? Did that object reference get changed as well? The answer is no. Immer knows exactly what data has been touched. So if we did an strict equality check after the component has been updated we can see that the previous user object and the next user object in state are exactly the same.
https://gist.github.com/252e33ec9856e260a367612117c90398
This is huge for when performance might matter when you use shouldComponentUpdate
or need an easy way to know if a row has updated for something like FlatList
in React Native.
Immer can make operations even easier. If it sees that you are passing in a function as the first argument instead of an object it will create a curried function for you. So rather than the a new object the produce
function returns another function.
It will take the first argument when it's called and use that as the state
you want to mutate, and then additionally any other arguments will also be passed along.
So rather than a function we could create a counter function and the props
will be proxied along.
https://gist.github.com/66d583f49f3fc64e6442c5cafc0e62f0
Then because produce
returned a function we can pass this directly into our setState
which has the ability to take a function. You should use a functional setState
when you are referencing your previous state, and in our case we need to reference the previous count to increase it to it's new count. It will pass in the current state and props which is exactly what we've set our counter
to expect.
So our new interval will just have this.setState
receiving counter
which is a function.
https://gist.github.com/8afc36cd9676be94f2c7f0cebdb7cb9f
This is obviously a contrived example but has huge real world applications. Long lists of data where a single field is updated can be easily compared and only the single row updated. Large nested forms only need to update the specific parts that were touched.
You no longer have to do a shallow or deep comparison and can now do a strict equality check and know for certain whether or not your data has actually changed and if you need to re-render.