=== on two objects doesn't look inside them. It asks whether they're the same object, which is called reference equality, or identity. Two objects created separately are never ===, however alike they are. Deep equality, also called structural equality, compares what's inside instead: two values are deeply equal when they have the same shape and the same contents, compared all the way down through nested objects and arrays.
Think of two printed copies of the same book. === asks whether you're holding the very same copy. Deep equality asks whether every page says the same thing.
const a = { name: "Ada", tags: ["math", "code"] };
const b = { name: "Ada", tags: ["math", "code"] };
const c = a;
a === b; // false: two objects, created separately
a === c; // true: c is the same object as a
a.name === b.name; // true: strings compare by value
a.tags === b.tags; // false: two arrays, even with the same contents
| Values | === | Deeply equal? |
|---|---|---|
{ x: 1 } and { x: 1 } | false | Yes |
[1, [2, 3]] and [1, [2, 3]] | false | Yes: the nested arrays match too |
{ x: { y: 1 } } and { x: { y: 2 } } | false | No: they differ one level down |
a and a, one object | true | Yes |
Tests rely on it all the time. In Jest and Vitest, expect(a).toEqual(b) checks deep equality, while expect(a).toBe(b) checks with Object.is, which for objects means the same object. Lodash's isEqual and Node's util.isDeepStrictEqual are other ready-made versions. Deep equality costs more than ===: it may have to visit every value in both.
React uses identity instead: it compares hook dependencies and memo props with Object.is. So a new object with the same contents counts as a change, and an object changed in place doesn't.
What counts as the same contents is a choice each version makes, for things like NaN, dates, Maps, or two objects with different prototypes. That's why a task that asks for deep equality spells out its rules.
React skips re-rendering a memoized component when its new props are shallowly equal to its old ones. Write shallowEqual(a, b), which returns true when a and b are shallowly equal, and false otherwise.
If Object.is(a, b) is true, they're equal. Otherwise, both must be objects, not null, with the same own enumerable string keys (the ones Object.keys lists), in any order. For every key, the two values must be the same by Object.is.
Don't look inside the values: two different objects with the same contents are different values. Arrays are objects too, so two arrays are compared item by item.
const style = { color: "red" };
shallowEqual({ id: 1, style }, { id: 1, style }); // true
shallowEqual({ id: 1, style }, { id: 1, style: { color: "red" } }); // false: a different object
shallowEqual({ a: 1, b: 2 }, { b: 2, a: 1 }); // true
shallowEqual({ a: 1 }, { a: 1, b: undefined }); // false: the first has no b
shallowEqual([1, 2], [1, 2]); // true
shallowEqual(NaN, NaN); // true
expect(shallowEqual({ a: 1, b: "x" }, { a: 1, b: "x" })).toBe(true);
expect(shallowEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true);
expect(shallowEqual({}, {})).toBe(true);
const style = { color: "red" };
expect(shallowEqual({ style }, { style })).toBe(true);
expect(shallowEqual({ style }, { style: { color: "red" } })).toBe(false);
expect(shallowEqual({ list: [] }, { list: [] })).toBe(false);
expect(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 3 })).toBe(false);
expect(shallowEqual({ a: 1 }, { a: "1" })).toBe(false);
expect(shallowEqual({ a: null }, { a: undefined })).toBe(false);
expect(shallowEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
expect(shallowEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false);
expect(shallowEqual({ a: undefined }, { b: undefined })).toBe(false);
expect(shallowEqual({ a: undefined }, {})).toBe(false);
expect(shallowEqual({}, { a: undefined })).toBe(false);
expect(shallowEqual({ a: NaN }, { a: NaN })).toBe(true);
expect(shallowEqual({ a: 0 }, { a: -0 })).toBe(false);
expect(shallowEqual(NaN, NaN)).toBe(true);
expect(shallowEqual(0, -0)).toBe(false);
const props = { a: { b: 1 } };
expect(shallowEqual(props, props)).toBe(true);
expect(shallowEqual(1, 1)).toBe(true);
expect(shallowEqual("a", "a")).toBe(true);
expect(shallowEqual(1, "1")).toBe(false);
expect(shallowEqual(null, null)).toBe(true);
expect(shallowEqual(null, {})).toBe(false);
expect(shallowEqual({}, null)).toBe(false);
expect(shallowEqual(undefined, null)).toBe(false);
const item = { id: 1 };
expect(shallowEqual([1, 2], [1, 2])).toBe(true);
expect(shallowEqual([item], [item])).toBe(true);
expect(shallowEqual([1, 2], [1, 2, 3])).toBe(false);
expect(shallowEqual([{ id: 1 }], [{ id: 1 }])).toBe(false);
expect(shallowEqual(Object.create({ a: 1 }), {})).toBe(true);
expect(shallowEqual({}, Object.create({ a: 1 }))).toBe(true);
const inherited = Object.assign(Object.create({ a: 1 }), { b: 1 });
expect(shallowEqual({ a: 1 }, inherited)).toBe(false);
const hidden = Object.defineProperty({ a: 1 }, "secret", { value: 2, enumerable: false });
expect(shallowEqual(hidden, { a: 1 })).toBe(true);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with shallowEqual. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.