DEV Community

Shinsuke Marty Yamada for basal.

Posted on

Immutable data management in single state applications

In this post, I'm going to explore application state management with immutable data and related JavaScript data types. I will be focusing on the "Reducer" of the popular Redux library which is known for application state management.

What state management is

Recently, the concept of state management has been taking a significant and essential role in single page application development. Some would say that Redux was the library which sparked the concept in our industry. Many developers would agree, it allows them to create a complicated application in a simple way and makes it easier to see the current status of the application. As a result, our code is easier to debug. Even though it is so powerful; Redux itself is built with really simple concepts.

What simplicity does Redux bring to Javascript applications? I'd like to focus on "Reducer" which is one of the important elements included in Redux. Reducer is the only interface using a pure function to update the state. To update the state, Reducer takes the current state and difference data called Action and generates a new state.

reducer

What I specifically mention here is that Reducer doesn't update the state directly, but generates a new state. This state generated, is independent of the state before updating. This means that the previous state won't be changed after generating a new state. With that in mind, it enables us to easily trace how the state changes the application has been gone within any given time sequence. Generally, such data that cannot be changed after generation is called "immutable".

state transition

Immutable data in JavaScript

Before moving to the actual topic, I'd like to mention immutable data in JavaScript briefly. Primitive type data such as String, Number and Boolean cannot be changed after creation. On the other hand, Reference type data such as Object and Array can be updated after creation. Those are called "mutable".

var a = 1; var b = { k: 1 }; var c = [1]; a = 2; // this is not changing 1 but creating 2 newly and re-assigning to a b.k = 2; // can change a property c[0] = 2; // can add new value console.log(b); console.log(c);

Looking at the example above, we can see that the object { k: 1 } and the array [1] can be changed. You might see that the number assigned to the variable "a", is also changeable, however, this is not changing "1" itself, but creating "2" and re-assigning it to "a" (The value of the variable "a" has overridden by "2").

var b = { k: 1 }; var c = [1]; Object.freeze(b); Object.freeze(c); b.k = 2; // this doesn't work c[0] = 2; // this doesn't work console.log(b); console.log(c);

There is a method to make objects and arrays immutable. That's to use Object.freeze(). In the above example, the value of the object and the array are made unchangeable by applying for Object.freeze().

Immutable variable

New variable definition statements, "const" and "let" are newly added in ES6. By using const to define a variable, we can define a variable that cannot be reassigned. In the example below, you will get an error if you try to reassign a value to the variable b defined by using const. With that, we can prevent unintentional variable reassignment before it happens.

let a = 1; a = 2; // You can assign 2 to a const b = 1; b = 2; // You cannot assign a value (Throw an error)

What we should take care with when defining an object with const is that the properties of the object are still mutable. Const only disables reassignment of a variable and it doesn't make the object immutable. To make an object immutable, we need to apply Object.freeze().

const c = { k: 1 }; c.k = 2; // You can change the property c.l = 3; // You also can add a new property console.log(c); const d = { k: 1 }; Object.freeze(d); d.k = 2; // You cannot change the property d.l = 3; // You also cannot add a new property console.log(d);

In a nested structure

I have introduced Object.freeze() as a way to make objects or array immutable. Actually, I need to tell you one thing that you need to care with when dealing with nested structures. Let's see the example below. We can see we are still able to change the value of a nested object even though we apply Object.freeze(), it is only abled to the object, not to the nested structure. This is because Object.freeze() works only for the top level object passed to it. If you want to make the whole values of the nested structure immutable, you have to apply Object.freeze() for each object.

const a = { k: { l: 1 } }; Object.freeze(a); a.k = { l: 2 }; // You cannot change it but... console.log(a.k); a.k.l = 2; // You can change the child value console.log(a.k); const b = { k: { l: 1 } }; Object.freeze(b); Object.freeze(b.k); a.k.l = 1; // You cannot change the value! (It's still 1) console.log(a.k);

Immutable application state management with Reducer

Now, let's move onto the main topic, state management. I'm going to dig into the implementation of Reducer which is the important element of Redux mentioned earlier.

reducer

As expressed in the above expression, Reducer updates the state by receiving the current state and Action which is the actual changes. In that process, Reducer generates a new independent state not changing the current state. Let's say we have the current state as an object { a: 1, b: 1, c: 1 } and Action as an object { b: 2 }. Reducer generates a new state and set 2 given by Action to 'b' of the new one. Since 'a' and 'c' aren't changed, those values are copied and set to the new one as they are. As a result, object { a: 1, b: 2, c: 1 } is returned as the new one. The update completes by swapping the new one with the current one. Therefore, Reducer enables us to manage our application state in an immutable way by generating a new state every time the state is updated.

state update

Reducer implementation example

The example code below is written based on the diagram above. As you can see, Reducer has a very simple structure only generating and returning a new state made with the current state and Action. On the 3rd line, it returns the value to the caller by overriding the new object passed to the first argument of Object.assign() with the order of the current state and the action. Since the state value generated there is stored in a different memory space from where the current state is, we can confirm the difference between the new one and the previous one when we compared them on the 17th line. As you might have already realized on the 15th line, we can still mutate the generated state like state.a = 2 directly unintentionally, however, those changes will be completely missing when swapping with the next state. Therefore, state updates always have to be done by Reducer.

function reducer(state, action) { // Generate new state return Object.assign({}, state, action); // or use spread operator of es6 like this // return { ...state, action }; } // Initialize state let state = { a: 1, b: 1, c: 1 }; // Create action const action = { b: 2 }; // Cache previous state const prevState = state; // Update state state = reducer(state, action); console.log(state === prevState); // false console.log(state.b); // 2

Conclusion

I introduced application state management with immutable data and related JavaScript data types while focusing on the state update function, "Reducer" of Redux. In the beginning, I mentioned about the simplicity brought by immutable data. Immutable data is unchangeable data after generated such as Number and String type. Also, it was mentioned that Object and Array type are called mutable. Apart from that, I mentioned that it is possible to make those mutable data unchangeable by applying for Object.freeze(). Finally, I introduced how Reducer actually generates a new state in an immutable with an example.
This time, I intentionally used very simple state data as an example easy to explain, however, actual state would look more complicated like using nesting. I'd like to introduce how to manage such a state by keeping in an immutable in my another post.

Top comments (0)