BAM

The redux best practice "Do Not Put Non-Serializable Values in State or Actions" explained

Intro

I never really understood one of the four Redux essential best practices, nor did I actually really try to. Until I had to learn it the hard way, when I was facing a weird bug. 

I had this JavaScript error after calling the getTime method of myDate, a persisted Date object:

Capture d'e?cran 2021-03-20 a? 10.28.46

As I did not understand where it came from, I did what most serious developers in this situation do: I ran my code again without making any change, hoping for it to work. It did not.

Actually, redux-persist documentation explains that they are not able to persist non-serializable values, like my Date object apparently is.

But that's fine because they should never have been in our stores anyway, persistence or not: it is a redux best practice: "Do Not Put Non-Serializable Values in State or Actions". Just like "Promises, Symbols, Maps/Sets, functions, or class instances", Date objects should not be saved in our stores. This is one of the four essential best practices.

Some reasons for this best practice were very well explained. But for some of them, I had to dig a little bit further. So I thought I'd share what I've learnt along the road.

First, let's make a quick recap.

Let's align on what serialization means

Serialization is the process of formatting a data to fit another data structure, to be able to store it and retrieve it later.

In our case, we are talking about JSON-serialization. 

To serialize an object to JSON, you can use JSON.stringify:

++pre>++code>const myStringifiedObject = JSON.stringify({ it: 'works' }); // {"it":"works"}++/code>++/pre>

And to turn your object back to JS, you can use JSON.parse:

++pre>++code>JSON.parse(myStringifiedObject); // { it: 'works' }++/code>++/pre>

But those methods don't support every type of data. For example, Sets don't have their own representation in JSON, and will be turned into an empty objects:

++pre>++code>const myStringifiedSet = JSON.stringify(new Set([1, 2, 3])); // {}++/code>++/pre>

Which means that when you retrieve it, you get an empty object:

++pre>++code>JSON.parse(myStringifiedSet); // {}++/code>++/pre>

You lost your information!

Those types of data that don't have any representation in JSON are called non-serializable. If you serialize and then unserialize those types of entities, you won't end up with the correct data structure, and may loose some informations.

Now that we are clear about what serialization means, let's get to the heart of the matter.

There are three reasons why you should not put non-serializable values in your state nor in your actions.

Reason 1: To make sure the UI gets updated as expected

In the definition of the best practice, it is written that not using non-serializable values "ensures that the UI will update as expected". Let's take a look at what it means in short, and we will break things down afterward:

A non-serializable value is a complex object, like a class instance or a function. It is not an array, a plain serializable object, nor a primitive (like strings, numbers, booleans, null, etc.). Otherwise, it would be included in the list of the items that JSON supports.

As it is a complex object, you have more chances to mutate it. (I'll explain why in a sec).

If you mutate an object, redux might not be able to trigger a re-render on your React component, and you might therefore have old, incoherent data in your UI.

Now that we have the big picture, let's dig a little bit further: first, why would we especially end up mutating non-serializable data?

You would mutate your non-serializable data

Let's say you have a Set of numbers in your store. You want to be able to add a new number to the Set. How do you handle this in your reducer?

Well, you certainly cannot use the Set's add method, as it would mutate your Set. Nor do you have access to any of the very accommodating JavaScript array functions, map, filter, etc. As they all return a new array with no mutation, they are perfect to be used in a reducer, but unfortunately, there is no equivalent on Sets.

What you could do though, is to turn your set into an array, use your favorite JavaScript array function to add your number, and turn this one into a Set. And there you go, you've got your new Set!

++pre>++code data-line-start="45" data-line-end="47">const newSet = new Set([...mySet, newNumber])
++/code>++/pre>

That is the opposite of efficiency. And it's exhausting. This is why one of your development team member would be more at risk of mutating your Set by simply doing:

++pre>++code data-line-start="45" data-line-end="47">const newSet = mySet.add(newNumber)++/code>++/pre>

It is even more true for some other non-serializable items the doc mentions. Think of the easiest way to change a class instance without mutating it.

Feel free to check out other data structures in the list, you will come to the conclusion that you have more chances to mutate non-serializable data by accident, because you don't really have any other easy workaround. And the first Redux best practice is very clear: "Do Not Mutate State".

Mutation is evil for redux

In a previous article, we wondered what would happen if we mutated our state.

And one (and not the least) of the consequences of mutation is inconsistent UI: a mutation over a non primitive value will not trigger a re-render, you will still see in your component the previous value, before it was mutated. It would result in inconsistent UI: the UI doesn't reflect the data in your store.

Is this reason legitimate?

Mutation on a non-serializable value is not any different from mutation on an array. So one may wonder, is this UI update reason good enough to avoid non-serializable values?

Well, you can mutate an array, but arrays, and even objects, have plenty of methods here to help you create values easily without mutations, that non-serializable data don't have. The difference lays here, you are way more likely to mutate a class or a Set, because you can't really do otherwise, that's what you do with those objects.

But what if those are not valid arguments for you? If you do not need to mutate it, nor to painfully change it as we tried with our set? Well, maybe you don't need to store this value in your store at all.

Reason 2: You don't (always) need to save this type of data in a store

To know if I really need non-serializable data in my store, I first have to be able to identify them.

What are the non-serializable data 

Well pretty much nothing is serializable, except plain JS Objects, Arrays and Primitives. The best practice definition also gives us a list of examples: Promises, Symbols, Maps/Sets, functions, and class instances. Some of you might have noticed that the Date object I struggled with was actually in that list. Look closely.

Indeed, to initialize a Date object, I have to use the "new" keyword. That makes it a class instance. You have to be aware that every time you create a value by starting with the "new" keyword, it is actually an instance of some kind of class. So when they say "Promises and Symbols, Maps/Sets and class instances", they actually say "class instances".

So class instances are generally not serializable, and should not be found in our store. I say generally because there are exceptions: Objects and Arrays are serializable, and are perfectly safe to be put in our stores.

Plus, class instances are a perfect example of data types that you don't need in your store!

class instances workaround

Take my Date object for example. It has plenty of setter methods, like setDate. But they involve mutation, so I can not use them. The only really useful information is actually its timestamp. So I don't need to keep a Date object in my store, I can just save timestamp: it gives me as much information as a Date object would. And I can convert my timestamp into a Date when I have to use Date's methods.

We can generalize this reasoning to all class instances.

In a class, you have two kinds of elements: the interesting data (like the timestamp of my date), and a bunch of methods that will never change, they are just pure logic. Why would you save those in a store? Placing this unchanging logic in your store will be of no use. Instead, you can convert your methods into logic handled either directly by the reducer, or completely outside of your store, depending on what they do, and put the interesting data inside your store.

Serializable workarounds

If you are interested in further exploring what your favorite non-serializable data structure might look like if properly used in a store, this article gives you some workarounds to save only the interesting part in your store.

Most of the time, you don't need to save non-serializable data in your store, or at least there are some workarounds. That being said, there might also be some cases where you have no choice but to store non-serializable data.

What if I really need my non-serializable data?

Some non-serializable data cannot be avoided. It can be related to the use of a library, for example.
You might need it, and be very conscious about not mutating it. Are there still some counter-arguments, then?
As explained in the documentation, this best practice is just a guideline. As long as you know what you are doing with your data, and as long as you are not mutating it, you should be just fine! If you really need your non-serializable data, you can use a library  that enables you to use non-serializable data types, like Maps and Sets, and provides functions that don't involve mutation.

Nevertheless, you have to be aware that some redux extensions features might not work.

Reason 3: To be able to use redux extensions

This is actually a very important reason for the best practice, but it is not the one that required me to dig the most, as it is already very well documented, so I won't develop this part much.
What you need to know, is that redux extensions, like Redux Persist or Redux DevTools, expect you to follow this essential best practice, as they themselves need serialized data to work properly.

About redux persist

To be able to retrieve your values when the app is killed, or the machine turned off, redux-persist will store them in a storage engine, like the local storage on the web. On those engines, every item has to be stored as strings. So redux-persist is going to JSON-format your data, to change it into a string.

To do so, they call JSON.stringify when storing your value, and JSON.parse when retrieving it to transform it back to its original data structure.
So what happened to my beloved Date object?

Date object, like other non-serializable values, don't have any representation in JSON format. When JSON.stringify was called on my Date object, it returned a string timestamp:

++pre>++code>const myStringifiedDate = JSON.stringify(new Date('2000-01-01')); // "2000-01-01T00:00:00.000Z"++/code>++/pre>

This string was then stored in the local storage. When I retrieved my value, JSON.parse just returned the ISOString, not a Date:

++pre>++code>JSON.parse(myStringifiedDate); // 2000-01-01T00:00:00.000Z++/code>++/pre>

So when tried to call getTime on my variable, it threw an error, as this method does not exist on strings.

What the redux best practice encourages to do instead, is to store a string or number timestamp, and to convert it into a Date when needed.

About redux devtools

Another very important feature of redux, which depends on serialization, is time-travel debugging. And it is actually the reason why you should not have non-serializable actions, in addition to the state. The reason for that is very well explained here.

Conclusion

I hope the reasons for this best practice are more clear now. All of that being said, if you're not interested in redux extensions, and as long as you are careful about mutation, you can have non-serializable values in your store. In the end, this is just a guideline.
Just please consider using a timestamp next time you have to store a Date.

Développeur mobile ?

Rejoins nos équipes