DEV Community

Cover image for Equality in ReactJS - The ShallowEqual
Sean Oldfield
Sean Oldfield

Posted on • Updated on

Equality in ReactJS - The ShallowEqual

React is an incredibly powerful library for Frontend Applications but given its JavaScript base it's important to understand the nuances of equality between types. I've seen plenty of code in the wild where the nuances haven't been understood, these have had issues ranging from "laggy" apps to accidentally DDOSing micro-services. 😕

Introduction

Quick recap of React Rendering.

A component re-renders when one of 3 conditions is met:

  1. Its props change.
  2. Its state changes.
  3. Its parent re-renders.

For this post I'm going to focus on Point 1, "Its props change".

shallowEqual

Before we can get anywhere we have to ask what does "change" mean?

Let's look at the source code of React itself and see!

The shallowEqual

/**
 * Performs equality by iterating through keys on an object and returning false
 * when any key has values which are not strictly equal between the arguments.
 * Returns true when the values of all keys are strictly equal.
 */
function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (Object.is(objA, objB)) {
    return true;
  }
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  if (keysA.length !== keysB.length) {
    return false;
  }
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !Object.is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }
  return true;
}
Enter fullscreen mode Exit fullscreen mode

This code is run when React's Reconciler determines whether or not a component should update based on changes in props (the same style of check is also used in React for equality elsewhere but I'm going to focus on props here). The first argument objA will be previous props and the second objB is the next.

Object.is()

The key thing to understand here is this line in the for loop check:

!Object.is(objA[keysA[i]], objB[keysA[i]])
Enter fullscreen mode Exit fullscreen mode

What React is doing is seeing if a certain prop is the same as the same in the next props with Object.is.

Object.is is a strict check of equality; are two things the same (semantically different to equal).

Object.is works exactly how you might expect on primitive types, undefined and null.

Object.is(1, 1) // true
Object.is('Hello World', 'Hello World') // true
Object.is(true, true) // true
Object.is(undefined, undefined) // true
Object.is(null, null) // true
Enter fullscreen mode Exit fullscreen mode

The pitfall many fall into is on referential types: objects, arrays, and functions. Object.is will check the memory reference of these; only returning true if they are the same.

Object.is(['a'], ['a']) // false
Object.is({ a: 1 }, { a: 1 }) // false
Object.is(() => {}, () => {}) // false
Enter fullscreen mode Exit fullscreen mode

Each argument is a new reference to an object with the same values, which is why false is the result. If we went:

const array = ['a'];
Object.is(array, array); // true
Enter fullscreen mode Exit fullscreen mode

This time the reference is to the same array in memory and so Object.is returns true.

Component Rendering

But what does that mean for React Components?

Lets use an example (with typed props to be explicit 😁):

interface AppleProps {
  isBrusied: boolean;
  info: {
    type: string;
    color: 'red' | 'green';
  }
}
const Apple = ({
  isBruised,
  info
}) => (
  <div>{`Imagine I'm an apple! ${isBruised, info.type, info.color}`}</div>
);
Enter fullscreen mode Exit fullscreen mode

So we now have an Apple 🍎.

Let's say it's first rendered like this:

<Apple isBrusied={false} info={{ type: 'jazz', color: 'red' }} />
Enter fullscreen mode Exit fullscreen mode

And every subsequent render of parents has the apple with the exact same props, a red unbruised jazz apple 😋.

Just considering props changing, does the Apple re-render?

Unfortunately, it does, even though to us the apple's props are equal, they are not the same according to shallowEqual. info is the culprit due to it being an object. shallowEqual will always return that previous info does not equal next info as they reference different memory addresses.

This means the Apple would be constantly and unnecessarily re-rendering.

A potential solution

For the sake of not making this post too long, I won't delve into hooks like useCallback and useMemo which exist to help with this problem. They can be covered in a followup. This solution will ignore these.

From what we know around primitive vs reference types. Let's split info into its two primitives types. We would now have an Apple that looks like this.

<Apple isBruised={false} type={'jazz'} color={'red'} />
Enter fullscreen mode Exit fullscreen mode

This way if all three props stay the same the component won't render. You're now on your way to better optimized applications!

Conclusion

React's equality checks use strict checking when determining change. Primitive types work as expected but its important to remember that when using reference types like functions, objects and arrays they can cause unnecessary changes to your applications if you're not careful.

Thanks for reading 🎖! I hope to follow this post with a deep dive into useCallback and useMemo where this shallow equality also applies and why these hooks exist.

Oldest comments (1)

Collapse
 
romulomourao profile image
Romulo Mourão

Awesome! Looking forward to the next one