Equality in ReactJS - The ShallowEqual
Tags: react, javascript
Published: 2021-04-09 03:31:52

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](https://github.com/facebook/react/blob/master/packages/shared/shallowEqual.js) of React itself and see!

**The `shallowEqual`**
```js
/**
 * 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;
}
```

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:

```js
!Object.is(objA[keysA[i]], objB[keysA[i]])
```
What React is doing is seeing if a certain prop is the same as the same in the next props with [Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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`.

```js
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
```

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. 

```js
Object.is(['a'], ['a']) // false
Object.is({ a: 1 }, { a: 1 }) // false
Object.is(() => {}, () => {}) // false
```

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

```js
const array = ['a'];
Object.is(array, array); // true
```

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 😁):

```js
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>
);
```

So we now have an `Apple` 🍎.

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

```js
<Apple isBrusied={false} info={{ type: 'jazz', color: 'red' }} />
```

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.

```js
<Apple isBruised={false} type={'jazz'} color={'red'} />
```

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.