How Does Shallow Comparison Work In React?

March 27, 2022 - 5 minutes

Shallow comparison as a concept is all over React development. It plays a key role in different processes and can also be found at several places in the lifecycle of React components. Think of the mechanism to determine whether class components should update, the dependencies array of React hooks, memoization through React.memo, and the list goes on.

If you’ve read through the official React documentation at some point, it’s likely that you’ve seen the term shallow comparison mentioned quite often. But most of the time, it’s just a small note on its existence and rarely is it covered beyond that. So, this article will look into the concept of shallow comparison, what it exactly is, how it works, and finish with some interesting takeaways that you most likely didn’t know yet.

Diving Into Shallow Comparison

The most straightforward way to understand shallow comparison is by diving into its implementation. The respective code can be found in the React Github project in the shared sub-package. There, you’ll find the shallowEqual.js file that contains the code we’re looking for.

import is from './objectIs';
import hasOwnProperty from './hasOwnProperty';

/**
 * 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 (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;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }

  return true;
}

There are quite some things going on, so let’s split it up and go through the function step by step.

function shallowEqual(objA: mixed, objB: mixed): boolean {
	// ...
}

Starting at the function definition, the function accepts two entities that will be compared to each other. Contrary to TypeScript, this code uses Flow as the type checking system. Both function arguments are typed using the special mixed Flow type, similar to TypeScript’s unknown. It indicates that the arguments can be values of any type, and the function will figure out the rest and make it work.

import is from './objectIs';

function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }
	// ...
}

After that, the function arguments are first compared to each other using the is function from React’s internal objects. The imported function is nothing more than a polyfilled version of JavaScript’s Object.is function. This comparison function is basically equivalent to the common === operator, but with two exceptions:

  • Object.is considers opposite signed zeroes (+0 and -0) unequal, while === considers them equal.
  • Object.is considers Number.NaN and NaN equal, while === considers them unequal.

Basically, this first conditional statement takes care of all the simple cases: if both function parameters have the same value, for primitive types, or reference the same object, for arrays and objects, then they are considered equal by shallow comparison.

function shallowEqual(objA: mixed, objB: mixed): boolean {
	// ...

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

	// ...
}

After having dealt with all the simple cases where the two function parameters are equal by value or reference the same object, we want to go into the more complex structures (objects and arrays). However, the previous conditional statement could still leave us with primitive values if either of the parameters is a primitive value.

So to make sure that we’re dealing with two complex structures from this point forward, the code checks whether either parameter is not of type object or is equal to null. The former check makes sure we’re dealing with objects or arrays, while the latter check is to filter out null values since their type is also object. If either condition holds, we’re certainly dealing with unequal parameters (otherwise the previous conditional statement would’ve filtered them out), so the shallow comparison returns false.

function shallowEqual(objA: mixed, objB: mixed): boolean {
	// ...

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

	// ...
}

Now that it’s sure that we’re only dealing with arrays and objects, we can focus on shallow comparing those data structures. To do so, we’ll have to dive into the values of the complex data structure and compare them between the two function arguments.

But before we do that, an easy check we can get out of the way making sure that both arguments have the same amount of values. If not, they’re guaranteed not to be equal by shallow comparison and that can save us some effort. To do so, we use the keys of the arguments. For objects, the key array will consist of the actual keys, while for arrays the key array will consist of the occupied indices in the original array in a string.

import hasOwnProperty from './hasOwnProperty';

function shallowEqual(objA: mixed, objB: mixed): boolean {
	// ...

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }

  return true;
}

As the last step, we iterate over the values of both function arguments by key and verify them one by one to determine whether they’re equivalent. For this, the code uses the key arrays that were generated in the previous step, checks whether the key is actually a property of the argument using hasOwnProperty, and uses the same Object.is function from before the compare the values.

If it turns out that any key doesn’t have equivalent values between both arguments, it’s for sure that they’re not equal by shallow comparison. Therefore, we cut the for loop short and return false from the shallowEqual function. If all of the values are equivalent, then we can call the function arguments equal by shallow comparison and return true from the function.

Interesting Takeaways

Now that we understand shallow comparison and the implementation behind it, there are some interesting things that we can take away from that knowledge:

  • Shallow comparison doesn’t use strict equality, the === operator, but rather the Object.is function.
  • By shallow comparison, an empty object and array are equivalent.
  • By shallow comparison, an object with indices as its keys is equivalent to an array with the same values at the respective indices. E.g. { 0: 2, 1: 3 } is equivalent to [2, 3].
  • Due to the usage of Object.is over ===, +0 and -0 are not equivalent by shallow comparison and neither are NaN and Number.NaN. This also applies if they’re compared inside of a complex structure.
  • While two inline created objects (or arrays) are equal by shallow comparison ({} and [] are shallow equal), inline objects with nested inline objects are not ({ someKey: {} } and { someKey: [] } are not shallow equal).

After graduation, my career is entirely centered around learning and improving as a developer. I’ve began working full time as a React developer and I’ll be blogging about everything that I encounter and learn during this journey. This will range from improving communicational skills in a technical environment, becoming a better developer, improving technical skills in React and JavaScript, and discussing career related topics. In all of my posts, the focus will be on my personal experiences, learnings, difficulties, solutions (if present), and also flaws.

If you’re either interested in these topics, more personalised technical stories, or the perspective of a learning developer, you can follow me either on Twitter or at Dev to stay up to date with my blogposts. I’m always learning, so stay tuned for more stories! 🎉