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:
- Its
props
change. - Its
state
changes. - 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;
}
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]])
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
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
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
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>
);
So we now have an Apple
🍎.
Let's say it's first rendered like this:
<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.
<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.
Oldest comments (1)
Awesome! Looking forward to the next one