When Immer first came out I was confused. I kinda understood what I was reading but I couldn't really see what was so great about it. As always, nothing beats actual code you type yourself to experience how something works.
Here is, I believe, a great example: https://codesandbox.io/s/y2m399pw31
If you're reading this on your mobile it might be hard to see what it does. Basically, it's a very simple React app that displays a "todo list like" thing. The state (aka. this.state.tasks
) is a pure JavaScript array. The React components that display the data (e.g. <List tasks={this.state.tasks}/>
and <ShowItem item={item} />
) are pure (i.e. extends React.PureComponent
) meaning React natively protects from re-rendering a component when the props haven't changed. So no wasted render-cycles.
What Immer does is that it helps mutate an object in a smart way. I'm sure you've heard that you're never supposed to mutate state objects (arrays are a form of mutable objects too!) and instead do things like const stuff = Object.assign({}, this.state.stuff);
or const things = this.state.things.slice(0);
. However, those things are shallow copies meaning any mutable objects within (i.e. nested objects) don't get the clone treatment and can thus cause problems with not re-rendering when they should.
Here's the core gist:
import React from "react";
import produce from "immer";
class App extends React.Component {
state = {
tasks: [[false, { text: "Do something", date: new Date() }]]
};
onToggleDone = (i, done) => {
// Immer
// This is what the blog post is all about...
const tasks = produce(this.state.tasks, draft => {
draft[i][0] = done;
draft[i][1].date = new Date();
});
// Pure JS
// Don't do this!
// const tasks = this.state.tasks.slice(0);
// tasks[i][0] = done;
// tasks[i][1].date = new Date();
this.setState({ tasks });
};
render() {
// appreviated, but...
return <List tasks={this.state.tasks}/>
}
}
class List extends React.PureComponent {
...
It just works. Neat!
By the way, here's a code sandbox that accomplishes the same thing but with ImmutableJS which I think is uglier. I think it's uglier because now the rendering components need to be aware that it's rendering immutable.Map
objects instead.
Caveats
-
The cost of doing what
immer.produce
isn't free. It's some smart work that needs to be done. But the alternative is to deep clone the object which is going to be much slower. Immer isn't the fastest kid on the block but unlike MobX and ImmutableJS once you've done this smart stuff you're back to plain JavaScript objects. -
Careful with doing something like
console.log(draft)
since it will raise aTypeError
in your web console. Just be aware of that or useconsole.log(JSON.stringify(draft))
instead. -
If you know with confidence that your mutable object does not, and will not, have nested mutable objects you can use object spread,
Object.assign()
, or.slice(0)
and save yourself the trouble of another dependency.
Comments
I suggest to migrate to hooks and use tiny libraries to manage nested and complex state, for example the one specifically dedicated to complex state management https://github.com/avkonst/react-use-state-x and 2 hooks: useMap and useList from react-use package. Disclaimer: I am an author of the first lib.