JavaScript Equality Under the Lens: Enhancing React's Dependency Checks
JavaScript, a language of many faces, often presents concepts that are deceptively simple yet profound in their implications. Among these, understanding equality checks and the nuanced landscape of JavaScript types stands paramount. Whether you're just dipping your toes into the vast ocean of JavaScript or you're a seasoned sailor navigating through the stormy quirks of the language, grasping these concepts is crucial.
This blog post sets out to demystify a concept that's often sidelined but deserves a spotlight: JavaScript's identity and equality checks. Despite their critical importance, details on how these checks align with ECMAScript standards are not thoroughly addressed in either React's documentation or the MDN web docs. The ECMAScript Language Specification doc however, does a great job in explaining these concepts. You can find it here and hopefully build the next version of Chrome.
Starting with foundational principles and progressively delving into complex nuances, we aim to explore this topic thoroughly, concluding with a hands-on example to reinforce your grasp on the subject.
JavaScript Types: The Foundations
JavaScript, at its core, categorizes values into several types — boolean
, undefined
, null
, number
, string
, object
, and symbol
. Each type serves a unique purpose:
Boolean:
Represents a logical entity, having two values: true or false.
Undefined:
Denotes a variable that has been declared but not yet assigned a value.
Null:
Represents the intentional absence of any object value. It's often used to indicate that a variable should hold an object but currently does not.
Number:
Holds numeric values. JavaScript uses floating-point arithmetic for its numbers.
String:
Represents textual data. It's a sequence of characters. Practically every primitive has string representation of itself;.
Object:
Unlike the other types that are primitive, objects are collections of properties.
Symbol:
Introduced in ES6, symbols are unique and immutable data types used as identifiers for object properties.
Hear ye, hear ye!
Understanding these types is the foundation upon which we build the concept of equality in JavaScript.
The Enigma of Null and Undefined
Both null
and undefined
denote the absence of value in a variable, but they are used in subtly different contexts. undefined
is the JavaScript engine's way of saying, "This variable not only has no value, but also might not even exist (no name has been reserved in memory for that variable)." In contrast, null
is a deliberate assignment by the developer, saying, "This variable presently has no value but specifically holds an empty or non-existent object. So null
is an actual value, like number
or string
, the only difference being that the variable has no value by design (a name will be reserved in memory for that variable). "
Equality Checks: "== vs ==="
JavaScript offers two operators for equality checks: ==
(abstract equality) and ===
(strict equality).
==
checks for equality after performing any necessary type conversions. It means if the values being compared are of different types, JavaScript will try to convert them to a common type before making the comparison. So"1" == 1
will return true since the value 1 which is a number (no double quotes), will be converted to the value 1 as a string (with double quotes).===
, or strict equality, checks for value equality without any type conversion. If the values being compared are of different types, the equality check immediately returns false. So in the previous example"1" === 1
will return false sincestring
andnumber
are different types.
Understanding these distinctions is vital as they often lead to unexpected results, especially for beginners.
Identity vs. Equality: The Subtlety
In JavaScript, values can either have identity (complex data types like objects, where the value is a reference to a location in memory) or be without identity (primitive values). PAUSE.
What in Sam's hill does it mean to have a value with or without identity? Well, let's dive into that mystery together.
A value with identity basically means a value that includes an extra property whose value is unique to that specific value. Same way your GitHub username is unique to you, and there can never be a situation where someone else might happen to have another account using your username.
A value without identity is a value doesn't have this extra property that declares its uniqueness.
So to put this into even better context, when you declare a variable whose value is an object the object automatically becomes a value with identity, the unique property being the memory location of the variable:
const variable = {name: "I am an object"};
When you however declare a variable whose value is a primitive, the value itself is the unique property (instead of the memory location):
const variable = 1;
When comparing primitive values (string
, number
, boolean
, undefined
, null
, symbol
), JavaScript checks their equality by value (which now in this case, is the value itself). However, when comparing objects, JavaScript checks whether they reference the same location in memory — which now is the property that uniquely identifies the object.
So when checking for equality for objects, comparing two objects with the exact same properties and the exact same values for those properties will still return false since these objects have different locations in memory.
const myObjectOne = {name: "I am unique!"};
const myObjectTwo = {name: "I am unique!"}
console.log(myObjectOne === myObjectTwo); // returns 'false' since each variable has it's own memory location.
console.log("-----------Boundary----------");
const myObjectOneAgain = {name: "I am unique!"};
const myObjectTwoAgain = myObjectOneAgain;
console.log(myObjectOneAgain === myObjectTwoAgain);// returns 'true' since the second variable's value is the first variable's memory location.
// So basically, the value of [myObjectTwoAgain] resolves to the memory address of [myObjectOneAgain].
console.log("-----------Boundary----------");; // prints out 'false' since each variable has it's own memory location.
Practical Application: useObjectEqualityChecker
Now, let's blend theory with practice through a React hook, useObjectEqualityChecker
, designed to check object equality by comparing their key-value pairs rather than their memory reference.
import { useMemo, useRef } from "react";
export type ObjType = { [key: string | number | symbol]: any; };
const useObjectEqualityChecker = (obj: ObjType) => {
const refObject = useRef<ObjType>({});
return useMemo(() => {
if (obj) {
const keys = Object.keys(obj);
let valuesMatch = true;
keys.forEach((k) => {
if (refObject.current[k] !== obj[k]) {
valuesMatch = false;
}
});
if (!valuesMatch) {
refObject.current = { ...refObject.current, ...obj, };
}
}
return refObject.current;
}, [obj]);
};
export default useObjectEqualityChecker;
This hook demonstrates a way to check equality beyond mere reference checking. It iteratively compares each key-value pair of the current and previous objects, updating the reference only when a discrepancy is found. This approaches equality checking with more nuance, especially valuable in React, where re-renders should be minimized for performance.
If you want even more sophisticated equality checks like deep comparisons, there's the: lodash.iseQual library that'll do this for you out of the box. At least now you do have a bit of clarity on what's happening under the hood, so there's no harm in using a library.
Wrapping Up
Equality in JavaScript is a multifaceted concept, laced with subtleties that can lead to nuanced bugs and performance issues if misunderstood. By unraveling these intricacies, from different types to identity checks, and applying them in practical scenarios like the useObjectEqualityChecker
hook, developers can write more efficient and bug-free code. Understanding equality deeply not only aids in mastering JavaScript but also in harnessing the full power of frameworks such as React.
And there you have it — a journey from the foundational to the advanced, all within the realm of JavaScript's equality checks. May this guide serve as a beacon, illuminating the path for both neophytes and veterans of JavaScript alike.
Cheers!