Learn in 5 Minutes: Understanding Immutability, Shallow and Deep Comparisons & Their Effect on Reactivity

While learning state management in React, I realized that understanding immutability is essential. Without grasping what immutability is and how it impacts state management, you can’t fully appreciate React’s reactivity. This led me to write this article. If you’re a detail junkie like me, you’ll love diving into this topic!

What is Reactivity?

In frontend development, reactivity refers to detecting changes in the data and updating the DOM accordingly. Different JavaScript frameworks and libraries follow various strategies to handle reactivity efficiently. Immutability helps us detect changes more effectively.

What is the Immutability Principle and Why Do We Practice It?

Immutability means that once a data structure is created, it should not be mutated. Instead, a newer version of it should be created and the reference of the data would be changed this way.

This principle helps us avoid side effects, simplifies change detection, and enhances performance.

A Quick Primer on Data Types and Equality in JavaScript

In JavaScript, primitive data types hold values and are inherently immutable, while non-primitive data types (like objects and arrays) hold references.

Checking if a primitive value has changed is straightforward — just compare the values.

For non-primitive values, things get trickier, and immutability becomes crucial. When dealing with objects and arrays, since they hold references, both loose equality (==) and strict equality (===) operators compare references, not values. This means that even if two objects look the same, they are considered different if their references are not the same. Essentially, these operators check references to determine equality or changes.

console.log(5 == '5'); // true (type coercion: string '5' is converted to number 5)
console.log(5 === '5'); // false (different types: number vs string)

const obj1 = { name: "Ahmet" };
const obj2 = { name: "Ahmet" };
const obj3 = obj1; // Same reference as obj1

console.log(obj1 == obj2); // false (different references)
console.log(obj1 === obj2); // false (different references)
console.log(obj1 == obj3); // true (same reference)
console.log(obj1 === obj3); // true (same reference)        

Shallow Comparison

You might wonder why a beginner-level topic like data types and equality checks is included in an advanced article about immutability and reactivity. The answer is simple: Shallow Comparison is an advanced evolution of equality operators for non-primitive data types. It checks references between previous and next versions of the data and inspects top-level elements to see if they have changed. Inspecting top-level elements means:

  • On arrays, it checks whether any element pushed, deleted, or reference to them is changed.
  • On objects, it checks top-level properties (not nested deep properties).

const user = { name: 'Ahmet', address: { city: 'Sakarya' } };

// Mutable way: Changing `name` and deleting `address` property

// Change the `name` property
user.name = 'Ali';

// Delete the `address` property
delete user.address;        

In this example, the shallow comparison still would be able to detect that user object is changed.

But it wouldn’t be able to notice changes in nested properties like this:

const user = { name: 'Ahmet', address: { city: 'Sakarya' } };

// Change the nested `city` property
user.address.city= 'Istanbul';        

And when React would check if the userobject is changed with shallow comparison, it wouldn’t be able to notice the change and wouldn’t be able to provide reactivity and re-render the component.

Deep Comparison vs. Immutability

Deep comparison checks properties recursively till the bottom and finds nested changes too but it’s more complex and less performant. This is where immutability proves its worth.

By adhering to the immutability principle, we ensure that whether our data changes are shallow or nested, we don’t mutate the original object. Instead, we create a new object, which changes the reference and allows shallow comparison to detect changes. This approach maintains data consistency, ensures predictable application behavior, and facilitates easy change tracking.

Immutable Way:

let user = { name: 'Ahmet', address: { city: 'Sakarya' } };
user = {
  ...user,
  name: 'Ali',
  address: {
    ...user.address,
    city: 'Istanbul'
  }
}; // Creating a new object and reassigning        

Approaches of React vs. Vue in Terms of Immutability and Reactivity

In React, immutability is a core principle for state management. We follow the immutability principle in every kind of state management. For example, in class components, we use setState; in functional components, we use the useState hook. Reducers in Redux also handle state with immutability.

In Vue, we aren’t forced to practice immutability. Instead, Vue allows us to mutate data directly. To maintain reactivity without enforcing immutability, Vue employs different strategies:

  • Vue 2: Uses JavaScript’s Object.defineProperty to create getters and setters for all data variables. When a property is accessed or mutated, Vue intercepts these operations and updates the reactivity system accordingly. However, Vue 2 has limitations with deeply nested properties. If you add, delete, or update nested properties, Vue 2 might not automatically detect these changes. We might need to use Vue.set to ensure that Vue detects changes in those cases.
  • Vue 3: Introduces a more modern and performant approach using JavaScript’s Proxy object. Proxies can intercept and react to operations on objects, such as property access, assignment, and deletion. This allows Vue 3 to handle reactivity for deeply nested objects more effectively and overcome the issues found in Vue 2. With Proxies, Vue 3 improves both performance and the ability to track changes in nested properties.

By leveraging these techniques, Vue provides flexibility in handling reactivity without enforcing immutability, which contrasts with React’s more rigid approach to immutability.

The differences between React and Vue extend beyond their approaches to immutability and reactivity. In general, React tends to provide developers with a set of manual tools and utilities, giving them more control but often requiring more configuration and decision-making. On the other hand, Vue integrates many tools and features directly into the framework, aiming to simplify the development process with built-in solutions.

For example, by default in React, if a parent component re-renders, its children also re-render, even if their props or state have not changed. To optimize performance and prevent unnecessary re-renders, React offers tools like React.memo, shouldComponentUpdate, useCallback, which allows developers to specify prop dependencies for child components, preventing re-renders if those dependencies do not change.

In contrast, Vue’s reactivity system handles this optimization by default. Vue efficiently tracks and updates only the components with changed properties, and reduces the need for manual optimizations. If you want to handle this manually, you can define your reactive data with shallowReactive and your data would be only checked with shallow comparison and it won’t create a deep watcher to catch nested changes.

Both frameworks have their strengths and trade-offs. React’s strict immutability and shallow comparison model offer predictability and performance but require developers to manage state changes meticulously. Vue’s more permissive approach with automatic reactivity handling, offers ease of use and integrated optimizations, though it might not always enforce immutability.

Conclusion

In summary, practicing immutability may dictate how we update our data but in return, it grants us some big benefits:

  • Immutability makes shallow comparisons a valid way to handle changes without any subsidiary solution needed. Since shallow comparison, as befits the name, makes shallow checks and doesn’t go deep checks, with immutability we ensure every change in our data also changes the reference of it.
  • Avoid side effects by not mutating objects directly.
  • Gives us a common practice for updating data, so that we can keep track of the changes in our code.

For more on immutability and state management in React, check out my article series on my React learning process. The next part (Part 3) will focus on state management in React.

To view or add a comment, sign in

More articles by Ahmet Otenkaya

Insights from the community

Others also viewed

Explore topics