Deep comparing object props in React not working as expected - javascript

I am optimizing performance of a React app by reducing unnecessary re-renders. The component I am working on receives a single prop containing an object with many keys including array of objects. I am using shouldComponentUpdate to check for changes in prop, but it's not working as expected:
shouldComponentUpdate(nextProps) {
if (!(_.isEqual(nextProps, this.props))) {
console.log('difference', _.differenceWith(nextProps, this.props));
return true;
}
return false;
}
isEqual is a method from Lodash for deep comparing objects. differenceWith is a Lodash method for finding difference in two objects. The weird thing is that using isEqual does not reduce re-renderings, and differenceWith prints an empty array []. But if I use JSON.stringify instead of isEqual, re-renders are reduced by half, but differenceWith still prints [].
shouldComponentUpdate(nextProps) {
if ( JSON.stringify(nextProps) !== JSON.stringify(this.props) ) {
console.log('difference', _.differenceWith(nextProps, this.props));
return true;
}
return false;
}
Why is there a difference in behaviour of isEqual and JSON.stringify when they are essentially doing the same thing, although in a different way (note that isEqual is also order sensitive)?
What is the best way to avoid re-renderings here?

1 - Why is there a difference in behaviour of isEqual and JSON.stringify when they are essentially doing the same thing, although in a different way (note that isEqual is also order sensitive)?
Take a look on what I found here.
Well that depends. For JSON.stringify(), the order matters. So if the key-value pair are ordered differently in the two objects but are the same, it will return false. Whereas it doesn't matter in Lodash isEqual, it will return true as along as the key-value pair exists.
const one = {
fruit: '🥝',
energy: '255kJ',
};
const two = {
energy: '255kJ',
fruit: '🥝',
};
// Using JavaScript
JSON.stringify(one) === JSON.stringify(two); // false
// Using Lodash
_.isEqual(one, two); // true
This means that you JSON.stringify can work, but not in all cases, so probably you shouldn't use it for your case.
I also found a benchmark that compares both. JSON.stringify is better for less deep nested objects, but _.isEqual gets better with more deep nested objects.
_.differenceWith is used to compare arrays wich receives 3 parameters, 2 are arrays to compare and the third one is the comparator.
Doing this _.differenceWith(nextProps, this.props) would be the same as _.difference(nextProps, this.props) as you can see in the docs
_.differenceWith
This method is like _.difference except that it accepts comparator which is invoked to compare elements of array to values. The order and references of result values are determined by the first array. The comparator is invoked with two arguments: (arrVal, othVal).
_.difference
Creates an array of array values not included in the other given arrays using SameValueZero for equality comparisons. The order and references of result values are determined by the first array.
So using one of this, will return a new array with the values different values. If it returns [] it means that the array have the same values but it can be in a different other.
_.differenceWith is good if you have an array of objects and you also want to compare the objects. This can be seen in the doc's example.
var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];
_.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual);
// => [{ 'x': 2, 'y': 1 }]
2 - What is the best way to avoid re-renderings here?
For your case, I recommend using _.isEqual because it compares exactly the order of an array and you can have objects with the same properties but in different orders, and it will be the same.
Use said
The weird thing is that using isEqual does not reduce re-renderings, and differenceWith prints an empty array []. But if I use JSON.stringify instead of isEqual, re-renders are reduced by half...
And I'm not sure why this happens, it deppends on alot of things, but you should definitely use _.isEqual if you want to compare state or props of a component.
You can also React.memo and React.PureComponent, this might help in some cases.
And as you can see in the docs of memo
If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
...
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs
And the docs of PureComponent
React.PureComponent is similar to React.Component. The difference between them is that React.Component doesn’t implement shouldComponentUpdate(), but React.PureComponent implements it with a shallow prop and state comparison.
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.

Related

Boolean comparison explanation [duplicate]

This question already has answers here:
How to compare arrays in JavaScript?
(61 answers)
Why are two identical objects not equal to each other?
(9 answers)
Closed 2 years ago.
I started with:
"1:2".split(':') == ["1","2"];
// false
Then tried:
[1,2] == [1,2];
// false
and ultimately:
[] == [];
// false
I've since found that:
"1:2".split(':').toString() == [1,2].toString();
// true
So I've solved my initial issue (kind of) but why can't arrays match each other?
Javascript arrays are objects and you can't simply use the equality operator == to understand if the content of those objects is the same. The equality operator will only test if two object are actually exactly the same instance (e.g. myObjVariable==myObjVariable, works for null and undefined too).
If you need to check if two array are equals i'd recommend to just traverse both arrays and verify that all the elements have the same value (and that the two array have the same length).
Regarding custom objects equality i'd build instead a specific equals function and i'd add it to the prototype of your class.
Considering that in the end you converted both arrays to a String and tested equality of the resulting strings, you could one day consider using a similar but more generic technique you'll find described in more than a few places:
JSON.stringify(OBJ1) === JSON.stringify(OBJ2)
Well, don't.
While this could work if the order of the properties will always the same for those object instances, this leaves the door open for extremely nasty bugs that could be hard to track down. Always favor a more explicit approach and just write a clean and readable function that will test for equality checking all the required fields.
The == operator for Objects in Javascript only checks to see if the objects are the same actual object reference, not if they are two separate object that contain the same contents. There is no built in operator for checking if they contain the same contents. You would have to write a function to do that sort of comparison yourself.
Your string conversion is one way of comparing two arrays as long as the array elements only contain primitive values (not other objects). If the array elements could contain other elements, then you would have to make sure those objects were themselves converted into representative strings too.
And, converting to a string would not discern between an array element that contains "4" versus one that contains 4 since both convert to "4" in the string representation.
FYI, you can read in this answer about records and tuples, two new data types coming to future Javascript that will allow comparing an immutable version of arrays and objects by value instead of by only comparing to see if they are the same object.
Equality for objects will tell you if the two objects are the same one.
var a = [];
var b = a;
a === b; // True, a and b refer to the same object
[] === []; // False, two separate objects
You will have to loop through the arrays to see if they have the same elements.
See: How to check if two arrays are equal with JavaScript?
In javascript each [] is an instance of window.Array class. So you are basically trying to compare two different objects. Since array's can have any no. and any type of elements including Objects and Custom Objects and those nested arrays can again have numerous properties and arrays and so on.
It becomes ambiguous when it comes to comparison, you will never be sure what do you want to do with those objects and nested properties. So what you are trying to achieve by comparing can be done in so many other ways. You just have to figure out the right way for your case.
One way is to make your own Array checking function:
How to compare arrays in JavaScript?!
Another way is to convert the Array to a String with .join(), and compare the strings. Then convert them back into Arrays with .split().
If I relate this problem with that in Python:
Input:
a="1 2 3 4"
case I:
a=input.split(' ')
output: ['1', '2', '3', '4']
case II:
a=map(int,input.split(' '))
output: [1, 2, 3, 4]
So, the fault is that of type, as it could come-out aa 'true' for:
"1:2".split(':').toString() == [1,2].toString(); //true

Most efficient way to mutate an item in array of objects? [VUEX]

I read the official Vuex example of how to mutate the state of element in array of objects data.
editTodo (state, { todo, text = todo.text, done = todo.done }) {
const index = state.todos.indexOf(todo)
state.todos.splice(index, 1, {
...todo,
text,
done
})
}
It seems a bit an overkill to find the index first (first loop), perform a splice and perform a copy with spread operator ...todo (would it be considered a second loop?).
Going by the docs:
When adding new properties to an Object, you should either:
Use Vue.set(obj, 'newProp', 123), or Replace that Object with a fresh one. For example, using the object spread syntax: state.obj = { ...state.obj, newProp: 123 }
There are only two ways to set / update properties to object.
In bigger data scenario that is causing some performance issues. Is there any better (more efficient) way to update an element in array of objects?

react native setState with array

Is there a way I could setState with arrays?
this.state={
numberOne:'',
numberTwo:'',
numberThree:'',
}
this.setState({numberOne:1})
this.setState({numberTwo:2})
this.setState({numberThree:3})
The code above will work but the code below wouldn't.
this.state={
numbers:['','','']
}
this.setState({numbers[0]:0})
this.setState({numbers[1]:1})
this.setState({numbers[1]:1})
How to I setState with arrays?
You can do it as follows:
this.setState({ numbers: [1, 2, 3] })
Or if you want to edit a specific array index, then you have to create a new copy of the array, without mutating the state:
const copied = [...this.state.numbers]
copied[0] = 1
this.setState({ numbers: copied })
Keep in mind that the copying can be done by value or reference, according to the array values types.
For booleans, strings, numbers array values it's is done by value (deep copying) and mutating the copied array values won't affect the origin one's values. So if this is your case - then you should not be worried about.
For objects array values - the copying is done by reference (shallow copying) and you have to be careful to not mutate the original array incidentally. You can read more about the array copying here.
In short: Whatever approach you take, always make sure you don't mutate the origin (state) array.
That syntax makes no sense. And you can't mutate the state directly. For changing individual array elements or object properties, you need to copy that state property first, modify that value, then replace the entire property. E.g:
// Initial state in constructor
this.state = { numbers: [1,2,3] };
Then to change it somewhere:
let newNumbers = this.state.numbers.slice(0); // makes copy of the array
newNumbers[1] = 5;
this.setState({numbers: newNumbers});
and then you state will be { numbers: [1,5,3] }

_.difference checks for object reference or checks property by property?

I've two arrays and I need to check they are not equal by their content (property by property)
Arrays and their current values are debugged in image.
Suggest any loadash function or javascript function to check deep equality which will exclude $$hashkey of array
Instead of lodash or javascript, there's method in angularjs to check equality of two objects: angular.equals & can be used as follows:
angular.equals(newValue, oldValue);
As it's angular's own equality check it doesn't have problem with $$hashkey it'll handle that (by not checking). Also it'll check equality by all property by property values with type checking also.
Plunker Example
You can use the _.isEqual function from Lodash. This function will compare all properties of your objects in your array.
var obj = [{ 'a': 1, b: 2 },{ 'a': 3, b: 4 }];
var other = [{ 'a': 1, b: 2 }, { 'a': 3, b: 4 }];
 
_.isEqual(obj, other); // => true
It's important to notice that the elements of both arrays need to be in the same order. This will return false:
var obj = [{ 'a': 1, b: 2 },{ 'a': 3, b: 4 }];
var other = [{ 'a': 3, b: 4 },{ 'a': 1, b: 2 }];
 
_.isEqual(obj, other); // => false
UPDATE
With _.isEqual you need the have two arrays in the same order. But to overcome this orderly comparison, you can use other lodash function, so in your case you can use like this (using sortBy):
_.isEqual(_.sortBy(obj, 'id'), _.sortBy(other, 'id'));
This will first sort the arrays by id and after verify if the arrays are equal. You can sort by multiple properties too, or by a custom function.

react - passing in a raw js array as a prop, but jsx is modifying it into a js object

Passing in a raw array as a prop, but it seems to be modified by React / jsx into a json object that has keys called "0", "1", etc, corresponding to the array elements. This makes it impossible to be able to tell if the prop is actually an array or a single component.
For example,
MyComponent has:
propTypes: {
attachedComponents: React.PropTypes.oneOfType([React.PropTypes.array, React.PropTypes.object])
}
Instantiate MyComponent and pass in an array:
var foo = <Foo /> // my custom component 1
var bar = <Bar /> // my custom component 2
<MyComponent attachedComponents={[foo, bar]} />
The problem, is that inside MyComponent, this.props.attachedComponents is not a real JS array -- it is some kind of JS Object that has keys "0", "1", each corresponding to the array element passed in.
This nmakes it impossible for me to programatically determine if it was a single component that was passed in, or if it was an actual array, without doing some really bad kludging:
MyComponent:
getInitialState: function() {
// this cannot work as intended, because the array passed in is converted into a js object whose typeof is object, not array:
if (typeof this.props.attachedComponents !== 'array') {
// do code for single component situation
}
}
I cannot check on Object.keys(this.props.attachedComponents).length, since, for a single component passed in, Object.keys(this.props.attachedComponents) looks like this:
["$$typeof", "type", "key", "ref", "props", "_owner"]
Now, if you are wondering why I am passing in arrays of components, it is because I want to have programmatic addition of components; I have seen this.props.children, but this does not seem reliable at all:
Facebook says that this.props.children is opaque, and you must use
React.Children api calls, all of which are getters, which seems to
imply that this.props.children should not be mutated.
Any ideas on how to detect if this.props.attachedComponents is an array, without doing some really bad kludging?
Thanks
JSX doesn't do anything to props.
It's literally just syntactic sugar for calling React.createElement. Your code gets converted to:
var foo = React.createElement(Foo, null); // my custom component 1
var bar = React.createElement(Bar, null); // my custom component 2
React.createElement(MyComponent, { attachedComponents: [foo, bar] });
Arrays are objects and typeof is weird. typeof someArray always returns "object":
console.log(typeof []);
The correct way to test for an array is to use Array.isArray:
console.log(Array.isArray([]));
console.log(Array.isArray({}));
I have seen this.props.children, but this does not seem reliable at all
Well, it is reliable, it just doesn't always return the same type of value. There are many helper functions available to process this.props.children. Have a look at React.Children. E.g. you can use React.Children.toArray(this.props.children) to convert the value to a true array.
The part you highlighted in the quote simple means that you shouldn't change the value of this.props.children itself. But that is not unreasonable in any way.

Categories

Resources