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
considersNumber.NaN
andNaN
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 theObject.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 areNaN
andNumber.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).