So you finally got a job you always dreamed of – as a React developer. There is a new project starting so you and your team have to set up everything. That’s a wonderful occasion to use some trendy tech and do the things right this time. You decide to use battle-tested react-boilerplate as a foundation of a frontend app. You clone the repo and start checking out an example app.
Actions, sagas, reducers… Wait, what? What is that strange way of setting properties on an object? Why you can’t use simple dot notation to access those properties? You start digging through dependencies and find a package called Immutable.js. It is responsible for all that fuss. Why did the react-boilerplate author decide to use it? Do you have to use it? Why you can’t write your reducers with plain old JavaScript? And what the heck is that immutability thing in the first place?
What is immutability?
Immutability is a concept best known from functional languages like Clojure or Lisp. It says that data can’t be mutated no matter what. But does it mean that once we create our data we cannot change it? No, it just means, that each time we want to change anything in our data we have to create complete copy with that change taken into account. We treat all data structures as working in read-only mode. Let’s see an example:
const arr = [1, 2, 3];
// we want to change second element in an array to 4,
// but we also want our data to be immutable, what can we do?
const changedArr = [...arr.slice(0, 1), 4, ...arr.slice(2)];
// or with object
const obj = {
foo: 'bar',
};
const changedObj = {
...foo,
bar: 'baz',
};
It doesn’t look very nice, right? Why this is better than writing arr[1] = 4? Let’s look closer and see why immutability matters and why it is so important in React/Redux world.
Why it matters?
Treating every data structure in our code as immutable gives us a high level of trust and confidence, that our code will behave exactly as we want. In JavaScript world, we are using mostly references to values, not values themselves. It means, that every code that has access to reference can change the value it references:
const foo = {
bar: 'baz',
};
someMysteriousFunction(foo); // do I really trust this function, that it will not mutate my object?
console.log(foo.bar); // can you tell for sure that it will log ‘baz’?
In the above example someMysteriousFunction
could possibly mutate foo properties. It means we can’t trust this function, we can’t trust any code that has access to the reference of that object. Unless it is immutable.
Immutability is particularly important in the context of Redux, because it enables Redux to work correctly and be fast. Redux (and react-redux) uses shallow equality comparison to make decisions about updates of state and components. To make this possible, Redux requires reducers to be pure functions with no side effects. It means that you have to return a new state from reducer, not mutated current state. This contract enables Redux to be fast and reliable. It also enables features like time travel debugging to work.
What is wrong with Immutable.js?
Now we know that immutability is a valuable concept and embracing it in our code will make it better. So why I think that using Immutable.js is not a good idea in most cases? Here I have to make a little digression and ask you to don’t get me wrong – I’m not trying to say that Immutable.js is a piece of garbage and you should avoid it. It is a superb piece of software written by very clever developers. I think that you should always choose the best tool for a particular task, and Immutable.js is one of those tools, but by design, it has some characteristics that I personally don’t like. Here they are:
- a lot of added boilerplate and no possibility to use standard syntax while working with objects and arrays – we have to use API
- most libs are not compatible with it, so we have to make extensive use of
fromJS
andtoJS
helpers - if you eventually change your mind and want to get rid of it, you will have a hard time doing this, because its own syntax is present in the whole codebase
- performance boost that it promises is rarely really observed in the vast majority of apps using it
- it makes development noticeably slower
In my humble opinion, the balance of profits and losses of using Immutable.js is not positive in most cases.
What can we do about it?
So, do we have to grit our teeth and use Immutable.js, or are there any alternatives? Luckily there are! I will briefly present some of them, that I find very appealing and easy to work with.
Object spread operator
Like we have seen in the previous example, we can use fairly new but standard object spread operator. It works similarly to Object.assign
static method, but is less verbose. It is available out of the box in all modern JavaScript runtimes and it also can be transpiled to older ES5 code using tools like Babel. Our reducer may look like this:
const reducer = (state, action) => ({
...state,
myChangingProp: action.payload,
});
As we see it is a very easy and elegant syntax. And we are not using any import – it works out of the box! The problem with it starts when we have more complex object tree and we want to change property which is deeply nested:
const reducer = (state, action) => ({
...state,
nestedObj: {
...state.nestedObj,
anotherNestedObj: {
...state.nestedObj.anotherNestedObj,
finallyAPropToChange: action.payload,
},
},
});
It starts to look cluttered. The solution will be to keep your state as flat as possible, what you should do anyway when working with Redux. If you don’t want to (or have to) work with a deeply nested object you can try some libs available through npm. I will briefly present to you two of them.
Immer
Let’s start with Immer. I will show you the same example with deeply nested object, but this time using Immer:
import produce from 'immer';
const reducer = (state, action) => produce(state, draft => {
draft.nestedObj.anotherNestedObj.finallyAPropToChange = action.payload;
});
It looks way cleaner! Finally we could use standard and familiar JS syntax to make changes in a state and still benefit from immutability. At first glance it seems that we are mutating our data and it’s in some sense true. We are mutating draft state, which is a proxy to our true state. This proxy records all changes and then creates new state with these changes included – that is what produce function is doing.
Moreover, we can extract information from state using selectors and pass it down to components without them knowing that we are using Immer in reducers (which is not the case with Immutable.js). This is a huge advantage – it doesn’t tie components code to yet another lib and makes them easier to reuse. If you want to know more about inner workings of Immer please check out their Github page: https://github.com/mweststrate/immer
seamless-immutable
We are moving on to the next – seamless-immutable. Again, let’s first see the code:
import Immutable from 'seamless-immutable';
const initialState = Immutable({});
const reducer = (state, action) =>
state.setIn(['nestedObj', 'anotherNestedObj', 'finallyAPropToChange'], action.payload);
As you can see, state is not a plain JavaSript object anymore, it is wrapped with Immutable factory. We also have to use API to make a change. Acknowledge, that this setIn method will not mutate state, it will return new state, that’s why we could use this as a return value of reducer. It looks a lot like code that uses Immutable.js, so why I think it’s better?
Although we have to use API to “mutate” data, we could use standard dot or bracket notation to access it, because seamless-immutable is backwards-compatible with plain JS objects and arrays. This is the same situation as with Immer – only reducers know that we are using seamless-immutable. Selectors, components, and other code “thinks”, that it is dealing with standard objects. Of course, there are some edge cases, when you will have to call toMutable
to get real object, but they are rare. You can read more about seamless-immutable here: https://github.com/rtfeldman/seamless-immutable
Conclusion
Surprisingly or not, you may not need Immutable.js to enforce immutability of your data structures. I use words “may not” instead of “do not”, because I think that Immutable.js is just a tool and you should treat it like one and use it wisely. You can and should use it, if your use case requires it, but keep in mind that it is not de facto standard and there are many alternatives. I showed you a few of them which I use and find easy and pleasant to work with. Let me know in the comments if you agree or disagree with me – constructive discussion is what makes us all more open minded and conscious about what we know and what we don’t know yet.
Sources:
https://medium.com/@Noitidart/reasons-i-dislike-immutable-js-7a11246fd31a
https://medium.com/dailyjs/the-state-of-immutability-169d2cd11310
https://redux.js.org/faq/immutable-data#how-redux-uses-shallow-checking
Author: Tymoteusz Dzienniak