4

I have a useEffect() that should trigger when an array of objects changes.

  useEffect(() => {
    setNotesToRender(props.notes)
  }, [props.notes]);

The useEffect dependency cannot compare arrays of objects, so the useEffect() in the example above triggers more often than needed. I only wish to trigger the useEffect() when the arrays differ.

An example of arrays that should trigger update

array1 = [{id:1,title:'one'},{id:2,title:'two'},{id:3,title:'three'}]
array2 = [{id:1,title:'one'},{id:2,title:'two'},{id:3,title:'four'}]

Things I have tested so far are:

  1. props.notes.length --> works but is not reliable - for obvious reasons :)
  2. ...props.notes --> does not work if array is empty - and may cause serious performance issues?

What is the most efficient way to compare two arrays of objects? The arrays may contain over 1000 objects.

Kind regards /K

UPDATE I need to filter out certain "notes" later using a text search, that's why I am using a React state. This is not shown in my example code.

Kermit
  • 2,865
  • 3
  • 30
  • 53
  • https://www.npmjs.com/package/react-fast-compare – Vaibhav Vishal Dec 04 '20 at 08:06
  • 1
    May be you can stringify the array of objects and add as dependency. Iam not sure whether this is efficient solution. – Vivek Dec 04 '20 at 08:07
  • 1
    JSON.stringify is fast, but will not work if order of keys are different in object. eg {a: 1, b: 2} will NOT equal {b:2, a:1}. – Vaibhav Vishal Dec 04 '20 at 08:08
  • 1
    Starting out with `props.notes` dependency is correct, then inside the effect do you deeper equality check per element in order to invoke `setNotesToRender`. For example, is it a difference an *any* element property, or only specific ones? You'll likely also need to implement the [usePrevious](https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state) react hook so you can do the deeper value comparison since react uses reference equality. – Drew Reese Dec 04 '20 at 08:18

3 Answers3

4

First of all 1k objects is not a large number of objects, but getting back to the question: the easiest way is to use

JSON.stringify(object1) === JSON.stringify(object2);

But it is not the most efficient, when performance is an issue, the smartest thing I could think of is to use a meta property like version and whenever we update the object in any way we increment the version as well. Then all you have to do is to check two arrays based on id + version mapping.

If the second option is not desired you could use lodash.deepCompare or create your own function, takea look at How to make data input readonly, but showing calendar? and simply check object by object while iterating.

Bogdan M.
  • 2,161
  • 6
  • 31
  • 53
  • 2
    `meta property like version and whenever we update the object`,, that's a good way of doing it, it's kind of how databases handle optimistic locking. And if the data is large it's certainly the most efficient way.. – Keith Dec 04 '20 at 09:27
4

What is the most efficient way

The most efficient way is likely a custom compare for this particular shape of data. You could use JSON.stringify or some other generic comparing function, but it's not likely to be the most efficient for obvious reasons.

So in your example a simple compare function might look like ->

const array1 = [{id:1,title:'one'},{id:2,title:'two'},{id:3,title:'three'}];
const array2 = [{id:1,title:'one'},{id:2,title:'two'},{id:3,title:'four'}];

const arrayIsEqual = (a1, a2) => 
  a1 === a2 ||
  a1.length === a2.length &&
  a1.every((f,i) => 
    f.id === a2[i].id &&
    f.title === a2[i].title
  )
  
  
//tests
const a1clone = [...array1];

//different arrays not equal
console.log(arrayIsEqual(array1, array2)); //false

//different arrays equal
console.log(arrayIsEqual(array1, a1clone)); //true

//different arrays same values, but different order
a1clone.reverse();
console.log(arrayIsEqual(array1, a1clone)); //false

//same arrays, fastest check
console.log(arrayIsEqual(array1, array1)); //true
Keith
  • 22,005
  • 2
  • 27
  • 44
  • 1
    Thank you for the excellent answer and example! \(^__^)/ I used your approach and it is very efficient! Many thanks! /K – Kermit Dec 06 '20 at 20:10
3

True, useEffect cannot compare array of objects so let the useEffect hook runs.

But what you can do is to add an if-statement whether JSON.stringified notesToRender and JSON.stringified props.notes are equal.(Or you can use lodash/isEqual) to prevent the set state method running.

However, if you think that is still not an efficient solution, we will have to see why do you need useEffect in the first place.

From your code, I think you are setting a useState hook based on the incoming props.

Thinking about that again, why would you not consider to directly render the props instead.

From official React doc page :
https://reactjs.org/docs/thinking-in-react.html#step-3-identify-the-minimal-but-complete-representation-of-ui-state

  1. Is it passed in from a parent via props? If so, it probably isn’t state.
  2. Does it remain unchanged over time? If so, it probably isn’t state.
  3. Can you compute it based on any other state or props in your component? If so, it isn’t state.
Kevin Moe Myint Myat
  • 1,916
  • 1
  • 10
  • 19
  • Hi Kevin! Thank you for the reply! Yes, in my example code the state is not needed. I forgot to mention that I need to filter out certain "notes" later using a text search, that is the reason for storing them in the state. /K – Kermit Dec 04 '20 at 09:26