How does shallow compare work in react
Asked Answered
D

10

137

In this documentation of React, it is said that

shallowCompare performs a shallow equality check on the current props and nextProps objects as well as the current state and nextState objects.

The thing which I am unable to understand is If It shallowly compares the objects then shouldComponentUpdate method will always return true, as

We should not mutate the states.

and if we are not mutating the states then the comparison will always return false and so the shouldComponent update will always return true. I am confused about how it is working and how will we override this to boost the performance.

Donothing answered 18/3, 2016 at 12:33 Comment(0)
C
176

A shallow comparison does check for equality. When comparing scalar values (numbers, strings) it compares their values. When comparing objects, it does not compare their attributes - only their references are compared (e.g. "do they point to the same object?").

Let's consider the following shape for the user object

user = {
  name: "John",
  surname: "Doe"
}

Example 1:

const user = this.state.user;
user.name = "Jane";

console.log(user === this.state.user); // true

Notice you changed the user's name property. Even with this change, the objects are equal. The references are exactly the same.

Example 2:

const user = clone(this.state.user);
console.log(user === this.state.user); // false

Now, even without any changes to object properties, they are completely different. By cloning the original object, you create a new copy with a different reference.

The clone function might look like this (ES6 syntax)

const clone = obj => Object.assign({}, ...obj);

shallowCompare is an efficient way to detect changes. It expects you don't mutate data.

Cassondra answered 18/3, 2016 at 12:51 Comment(10)
So If we are writing code then if we have scalar values then should we mutate them because if we will clone them, the equality check will return false?Donothing
@AjayGaur Although this answer can help you understand strict equality (===) in JavaScript, but it doesn't tell you anything about shallowCompare() function in React (I guess the answerer misunderstood your question). What shallowCompare() does is actually in the doc you provided: iterating on the keys of the objects being compared and returning true when the values of a key in each object are not strictly equal. If you still don't understand this function and why you should't mutate state, I can write an answer for you.Mod
It's not true. see this. github.com/facebook/fbjs/blob/master/packages/fbjs/src/core/…Tem
This answer is describing the difference between the equality (==) and strict equality (===) operators in JS. The question is about shallow comparison which, in React is implemented by checking the equality between all the props of two objects.Drice
@Mod can you please write an answer on that?Donothing
const clone = obj => Object.assign({}, ...obj); above code snippet is not working. it should be like this. const clone = obj => Object.assign({}, obj);Septicidal
"String" is not a scalar type!!Eonism
As of ES2018 syntax clone would be something like: const clone obj => { ...obj };Confectioner
The comment from @Mod is incorrect in some way. The correct behaviour of shallowCompare is: 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.Samovar
I personally don't understand how this answers the question, this is basically saying what is the === which is totally not what shallow compare is.Sabinasabine
H
55

shallow comparison is when the properties of the objects being compared is done using "===" or strict equality and will not conduct comparisons deeper into the properties. for e.g.

// a simple implementation of the shallowCompare.
// only compares the first level properties and hence shallow.
// state updates(theoretically) if this function returns true.
function shallowCompare(newObj, prevObj){
    for (key in newObj){
        if(newObj[key] !== prevObj[key]) return true;
    }
    return false;
}
// 
var game_item = {
    game: "football",
    first_world_cup: "1930",
    teams: {
         North_America: 1,
         South_America: 4,
         Europe: 8 
    }
}
// Case 1:
// if this be the object passed to setState
var updated_game_item1 = {
    game: "football",
    first_world_cup: "1930",
    teams: {
         North_America: 1,
         South_America: 4,
         Europe: 8 
    }
}
shallowCompare(updated_game_item1, game_item); // true - meaning the state
                                               // will update.

Although both the objects appear to be same, game_item.teams is not the same reference as updated_game_item.teams. For 2 objects to be same, they should point to the same object. Thus this results in the state being evaluated to be updated

// Case 2:
// if this be the object passed to setState
var updated_game_item2 = {
    game: "football",
    first_world_cup: "1930",
    teams: game_item.teams
}
shallowCompare(updated_game_item2, game_item); // false - meaning the state
                                               // will not update.

This time every one of the properties return true for the strict comparison as the teams property in the new and old object point to the same object.

// Case 3:
// if this be the object passed to setState
var updated_game_item3 = {
    first_world_cup: 1930
}
shallowCompare(updated_game_item3, game_item); // true - will update

The updated_game_item3.first_world_cup property fails the strict evaluation as 1930 is a number while game_item.first_world_cup is a string. Had the comparison been loose (==) this would have passed. Nonetheless this will also result in state update.

Additional Notes:

  1. Doing deep compare is pointless as it would significantly effect performance if the state object is deeply nested. But if its not too nested and you still need a deep compare, implement it in shouldComponentUpdate and check if that suffices.
  2. You can definitely mutate the state object directly but the state of the components would not be affected, since its in the setState method flow that react implements the component update cycle hooks. If you update the state object directly to deliberately avoid the component life-cycle hooks, then probably you should be using a simple variable or object to store the data and not the state object.
Hargrove answered 14/7, 2018 at 21:40 Comment(9)
Doesn't it mean that if I pass an object via props or compare state to next state, the component would never re-render because even if properties of that object have changed, it will still point to the same object, thus, resulting to false, thus, not re-rendering?Forcefeed
@Forcefeed - that is why you'd clone (using for example Object.assign()) your objects when they change instead of mutating them so React will know when the reference changes and the component needs updating.Pretender
If prevObj contains a key that newObj doesn't have, the compare will fail.Goldina
@Goldina - it won't because the "for in" iterates on newObj and not on prevObj. try running the code as is in browser developer console. Moreover please don't take this implementation of shallow compare too seriously, this is just to demonstrate the conceptHargrove
what about arrays?Perceivable
"For 2 objects to be same, they should point to the same object." <- this is confusing and wrong. For 2 objects to be the same, they should reference the same point in memory.Eonism
game_item and updated_game_item3 are any way different objects by reference. They will always return false. I didn't understand your premise of explanation.Hargreaves
@Hargreaves Shallow comparison is not about comparing the reference of 2 objects (current state and new state), instead its the comparison of the properties within those objects. In order to find out if 2 objects are the same (in spite of being 2 separate objects), React compares the properties of the 2 objects via "===". Thus if the property is a reference type (for e.g. arrays or objects) then they need to point to the same object and if the property is a primary type (for e.g. int or string) then they need have the same value.Hargrove
@Hargreaves Comparing with "==" would have sufficed but the problem is 22 == "22" returns true and hence "===" which returns false.Hargrove
C
45

Shallow compare works by checking if two values are equal in case of primitive types like string, numbers and in case of object it just checks the reference. So if you shallow compare a deep nested object it will just check the reference not the values inside that object.

Cheeseparing answered 6/11, 2018 at 9:40 Comment(1)
Such a simple and complete answer .. ThanksUpsydaisy
B
14

There is also legacy explanation of shallow compare in React:

shallowCompare performs a shallow equality check on the current props and nextProps objects as well as the current state and nextState objects.

It does this by iterating on the keys of the objects being compared and returning true when the values of a key in each object are not strictly equal.

UPD: Current documentation says about shallow compare:

If your React component's render() function renders the same result given the same props and state, you can use React.PureComponent for a performance boost in some cases.

React.PureComponent's shouldComponentUpdate() only shallowly compares the objects. If these contain complex data structures, it may produce false-negatives for deeper differences. Only extend PureComponent when you expect to have simple props and state, or use forceUpdate() when you know deep data structures have changed

UPD2: I think Reconciliation is also important theme for shallow compare understanding.

Brundisium answered 26/6, 2017 at 10:27 Comment(1)
shouldn't it be "false" in and returning true when the valuesMountebank
F
7

The accepted answer can be a bit misleading for some people.

user = {
  name: "John",
  surname: "Doe"
}

const user = this.state.user;
user.name = "Jane";

console.log(user === this.state.user); // true

This statement in particular "Notice you changed users name. Even with this change objects are equal. They references are exactly same."

When you do the following with objects in javascript:

const a = {name: "John"};
const b = a;

Mutating any of the two variables will change both of them because they have the same reference. That's why they will always be equal (==, ===, Object.is()) to each other.

Now for React, the following is the shallow comparison function: https://github.com/facebook/fbjs/blob/master/packages/fbjs/src/core/shallowEqual.js

/**
 * 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++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return

For non-primitives (Objects), it checks:

  1. If the first object is equal (using Object.is()) to the second.
  2. If not, it checks if each key-value pair in the first object is equal (using Object.is()) to that of the second. This is done for the first level of keys. If the object has a key whose value is another object, this function does not check for equality further down the depth of the object.
Figurative answered 30/11, 2020 at 10:27 Comment(1)
I think the first case hold for "primitive" Objects, as The Object.is() method determines whether two values are the same value developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… as Primitive are also Object but simplier dev.to/js_catch/…Vicereine
S
6

It took me a while to actually know shallow compare and === are two different thing, especially while reading redux documentation in the following.

However, when an action is dispatched to the Redux store, useSelector() only forces a re-render if the selector result appears to be different than the last result. As of v7.1.0-alpha.5, the default comparison is a strict === reference comparison. This is different than connect(), which uses shallow equality checks on the results of mapState calls to determine if re-rendering is needed. This has several implications on how you should use useSelector().

strict equal

so step by step, the strict equal === is very consistently defined by the Javascript language, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality

What it does is to compare two items by value if they are primitive, and then by reference if they are object. Of course if the types of two objects are different, they will never match.

shallow compare

shallow probably isn't a build-in feature of the language. Couple of the answers here pointed us to some variation of the implementation, https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/core/shallowEqual.js

The idea is to compare two items by value if they are primitive. But for non primitives, we go one level lower. For objects, if the keys are different between two objects, we say they are not same. If the value under a key is different, we say they are not same either.

summary

This means, shallow comparison checks more than the strict equal ===, especially when it comes to the object. A quick look might suggest, === does not do too much guess work.

Sabinasabine answered 21/2, 2022 at 19:17 Comment(2)
So for shallow comparison, if the value for a key is yet another object, is the procedure then recursively applied?Adjust
@SebastianNielsen, that's a very good question, based on the implementation I pasted, github.com/facebook/fbjs/blob/main/packages/fbjs/src/core/…, the answer is no, it doesn't do it recursively, for the first level, === is applied.Sabinasabine
G
3

The shallow equal snippet by @supi above (https://mcmap.net/q/129500/-how-does-shallow-compare-work-in-react) fails if prevObj has a key that newObj doesn't have. Here is an implementation that should take that into account:

const shallowEqual = (objA, objB) => {
  if (!objA || !objB) {
    return objA === objB
  }
  return !Boolean(
    Object
      .keys(Object.assign({}, objA, objB))
      .find((key) => objA[key] !== objB[key])
  )
}

Note that the above doesn't work in Explorer without polyfills.

Goldina answered 8/5, 2019 at 13:19 Comment(1)
Looks good but in this case passing two NaN's returns false while in previous answer it's true.Slaphappy
M
0

There is an implementation with examples.

const isObject = value => typeof value === 'object' && value !== null;

const compareObjects = (A, B) => {
  const keysA = Object.keys(A);
  const keysB = Object.keys(B);
 
  if (keysA.length !== keysB.length) {
    return false;
  }
 
  return !keysA.some(key => !B.hasOwnProperty(key) || A[key] !== B[key]);
};

const shallowEqual = (A, B) => {
  if (A === B) {
    return true;
  }
 
  if ([A, B].every(Number.isNaN)) {
    return true;
  }
  
  if (![A, B].every(isObject)) {
    return false;
  }
  
  return compareObjects(A, B);
};

const a = { field: 1 };
const b = { field: 2 };
const c = { field: { field: 1 } };
const d = { field: { field: 1 } };

console.log(shallowEqual(1, 1)); // true
console.log(shallowEqual(1, 2)); // false
console.log(shallowEqual(null, null)); // true
console.log(shallowEqual(NaN, NaN)); // true
console.log(shallowEqual([], [])); // true
console.log(shallowEqual([1], [2])); // false
console.log(shallowEqual({}, {})); // true
console.log(shallowEqual({}, a)); // false
console.log(shallowEqual(a, b)); // false
console.log(shallowEqual(a, c)); // false
console.log(shallowEqual(c, d)); // false
Malar answered 6/11, 2019 at 16:26 Comment(0)
C
0

Very simple to understand it. first need to understand the pure component and regular component, if a component has coming props or state is changing then it will re-rendered the component again. if not then not. in regular component shouldComponentUpdate by default true. and in pure component only the time when state change with diff value.

so now what is shallow component or shallow ? lets take an simple example. let a = [1,2,3], let b = [1,2,3],

a == b ==> shallow take it false, a == c ==> shallow take it true. c has any diff value.

now i think you can understand it. the diff in both regular and pure component with shallow component if you like it, also do like share and subscribe my youtube channel https://www.youtube.com/muosigmaclasses

Thanks.

Capitalization answered 22/3, 2021 at 7:22 Comment(0)
R
0

I feel that none of the answers actually addressed the crucial part in your question, the answers merely explain what shallow comparison is (whether they mean the JavaScript default shallow comparison that is a result of the === or == operator or React's shallowCompare() function)

To answer your question, my understanding so far of React makes me believe that yes indeed by not directly mutating the states then shouldComponentUpdate will always return true thus always causing a re-render no matter what objects we pass in setState even if the objects passed to setState hold the same values stored in the current state

example:

Say I have a React.Component with the current state and function:

this.state = {data: {num: 1}} // current state object
    
foo() { // something will cause this function to called, thus calling setState
       this.setState( {data: {num: 1}} ); // new state object
}

You can see that setState passed the same object (value-wise) however plain React is not smart enough to realize that this component shouldn't update/re-render.

To overcome this, you have to implement your version of shouldComponentUpdate in which you apply deep comparison yourself on the state/props elements that you think should be taken into consideration.

Check out this article on lucybain.com that briefly answers this question.

Rhigolene answered 6/5, 2021 at 18:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.