DEV Community

machy44
machy44

Posted on

Shallow comparison in Redux

One day at work I was trying to implement a new feature in my React/Redux project. I created a new reducer and asked myself if I took reducers as pure functions for granted. I was creating reducers as a pure function and I have never really asked myself why it must be pure (I admit I'm ashamed). I thought the only reason for this is to accomplish the undo history option in redux easier.

In this blog, I will try to explain why reducer shouldn't mutate the app's state. I will use the spread operator syntax.

Shallow comparison

Every time when you change something in the state you need to create a new object.That new object will have a new address in the memory. It means that we will pass an object by value, not by reference. You can see in the next JS code what does this mean.

//by reference
let first = {a: 1};
let second = first; // second shows on the same space in memory as the first
first.a = 2;
first === second; // shallow comparison will log true

console.log(first); // {a: 2}
console.log(second); // {a: 2}

//by value
let firstValue = {a: 1};
let secondValue = {...firstValue}; // we pass firstValue by the value
firstValue === secondValue; // shallow comparison will log false
firstValue.a = 2;

console.log(firstValue); // {a: 2}
console.log(secondValue); // {a: 1}

In Redux, a reducer is a function which does a certain job (it changes the app's state ). You can see this in an example below.

const initialState = {
    data: [],
    isError: false,
    isLoading: false
}

function exampleReducer(state = initialState, action) {
  switch (action.type) {
    case REQUEST_API:
      return { ...state, isLoading: true }; //creating a new state object
    case RESPONSE_API:
      return { ...state, isLoading: false, data }; //creating a new state object
    default:
      return state;
  }
}

Redux does the shallow comparison:

oldState === newState; // true or false

If a change has happened in the state (a new object is created) false value will be returned. This way, React/Redux knows if a component re-rendering needs to be triggered. It's more efficient to check this than doing a deep comparison or something else.

Sometimes we could encounter with deeply nested objects which can be hard to update. In this situation you can normalize the app's state or you can use immutable-focused libraries such as Immutable.js. Maybe I could write about this in some future posts.

Conclusion

In the end, it is easier to test pure functions.

It's always good to look at things under the hood and try to understand why things are the way they are.

If you have some thoughts to share or I missed something fell free to comment.

Top comments (2)

Collapse
 
misterwhat profile image
Jonas Winzen

Hey :)
great article. It is really clarifying why immutability is so important when writing reducers for state management.

I hope you don't mind if I correct you in one point:

In Redux, a reducer is a function which does a certain job (it changes the app's state ).

This is only partially correct. A reducer should never change the state of an app (thats mutation), but rather create the next state of an app, given an action + the last state.

To make sure reducers are not mutating existing state you could Object.freeze() your data structures that you use for your reducer unit tests. (keep in mind that Object.freeze() only shallow freezes an object. So you would need to recurse (see example) over nested structures, to completely protect them from mutations).
_Object.freeze() slows down property access, so don't use it in production 😉

Based on my past experience on using Redux for state management, I found it quite more comfortable to use Ramda for writing reducer logic, rather than Immutable.js.
While Immutable.js is a sophisticated and super fast library to work with immutable data, it is not very compatible with existing components and pieces of an App (assuming you are not starting entirely from scratch and never plan to use any existing library - ever).
As soon as you are in the need for plain old JS data, you might be tempted to star using the .toJS() functions of your immutable collections, to make it consumable for other APIs. .toJS() is quite slow and outperforms the benefits you have from using Immutable.js.
A compatible approach to building rock solid, composable reducer logic is to use Ramda (using lenses to combine selector and reducer logic in a directly composable format).

Collapse
 
machy44 profile image
machy44 • Edited

Sorry for the delayed answer. Yeah, you are right reducer create a new state. I had chosen the wrong words.
I have never used Immutable.js and Rambda so far. Thank you for giving me a lot of excellent information in your comment.