Rerender Problems in React Continued…

David Ryan Morphew
4 min readOct 19, 2021

--

Avoiding Unwanted Referential Equality with State Changes

In a previous blog, which you can find here, I discussed some rerending issues that you might encounter if you have one piece of state in React depend on the change of another piece of state.

In this blog, we’ll look at another frequent rerending issue that involves changes in state that React does not consider worthwhile to force a component to rerender.

The issue at hand concerns referential equality and shallow comparison.

Follow Along / Test the Examples

Take the following example.

Check out the Repo

You can can fork and clone this repo to follow along, or follow the same example below.

Example

import { useState, useEffect } from 'react';
import ChildComponent from './components/ChildComponent';
const initialArray = [
{number: 1},
{number: 2},
{number: 3},
{number: 4},
{number: 5}
]
;
const App = () => { const [myArray, setMyArray] = useState([]) useEffect(() => { setMyArray(initialArray) }, []) const reverseArray = () => {
const reversedArray = myArray.reverse()
setMyArray(reversedArray)
}
return(
<>
<button onClick={reverseArray}>
Reverse Order
</button>
<ChildComponent arrayData={myArray} />
</>)
}
export default App;

Here, we’ve set up a few things:

  1. We set up the piece of state called myArray and updated it after the component mounts (note that the second argument in the useEffect hook is “[]”) with a value of initialArray. The initialArray value is—aptly named—an array, and is full of objects: initialArray = [ {number: 1}, {number: 2}, {number: 3}, {number: 4}, {number: 5} ]
  2. We created a button that will fire the function called reverseArray.
  3. We defined the reverseArray function to take the state of myArray, reverse it, and then set the myArray value to that reversed order. So, when clicked the first time, it should reverse the order of the array to [ {number: 5}, {number: 4}, {number: 3}, {number: 2}, {number: 1} ].
  4. We have passed the myArray piece of state down as a prop called arrayData in the ChildComponent.

Will the Child Component Rerender when we reverse the order of myArray?

Photo by Daniel Herron on Unsplash

But why?

The change in our App’s state of myArray is compared with its previous state when passed down to the ChildComponent as a prop, but the comparison turns out to be too shallow in this case.

React and Referential Equality: Object.is

React uses the Object.is when checking the objects in the array in this example. The comparison made is shallow, looking to see if the object admits of “referential equality” to the previous state’s object. (You can check out more on this issue here and here.)

So, React, to be efficient in the rendering and rerending of components, does not bother to rerender the ChildComponent, treating the change in myArray as not significant enough to count as a state change that would force a rerender.

This:

[ {number: 1}, {number: 2}, {number: 3}, {number: 4}, {number: 5} ]

looks too similar (using the Object.is comparison of referential equality) to this:

[ {number: 5}, {number: 4}, {number: 3}, {number: 2}, {number: 1} ]

A Quick Fix

Did you notice anything fishy about how we reset state above (reproduced here):

const reverseArray = () => {
const reversedArray = myArray.reverse()
setMyArray(reversedArray)
}

Since JavaScript objects are pass-by-reference rather than pass-by-value, we actually mutated myArray before running setMyArray. This is demonstrated in the companion code:

  1. Open the application in the browser with npm start.
  2. Open up the devtools console.
  3. Click the “Reverse Order” button.
Screenshot demonstrating that myArray is mutated when reverseArray function is called.

COPY AND THEN MUTATE to Avoid Pass-By-Reference Side-Effects

As a good rule-of-thumb, it’s better to first copy an array before exerting any mutations on the data, so that you avoid any side-effects.

If we follow that rule-of-thumb here, using the spread operator like this:

const reverseArray = () => {
const reversedArray = [...myArray].reverse()
setMyArray(reversedArray)
}

then the value of reversedArray will be a new array. When we pass that into setMyArray, React will notice the change.

You can demonstrate this, once again, in the companion code by commenting out l. 15 and commenting in l. 16. Now, when you hit the “Reverse Order” button you can see that the original array of myArray is not being mutated with reversedArray, and is non-identical with it:

Screenshot demonstrating that the copied array is not identical with the original array and that the original array is not changed with the copy.

By creating a copy and mutating, and then passing that new mutated copy into our function that changes state, React will recognize a state change and actually do what we intended from the beginning:

The new state passed as a prop to the ChildComponent will cause a rerender of the ChildComponent.

Conclusion

There’s really no “trick” at play here. Instead, we are following a programming principle that we should have noticed we were violating from the beginning:

Avoid side effects when working with JavaScript objects, such as arrays, by first creating a copy of that object.

The unintentional mutation of the original array, myArray is a side effect. Following the principle of avoiding side effects such as this can help us avoid a problem of React ignoring a change in our state and props object due to referential equality.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

David Ryan Morphew
David Ryan Morphew

Written by David Ryan Morphew

I’m very excited to start a new career in Software Engineering. I love the languages, frameworks, and libraries I’ve already learned / worked with (Ruby, Rails,

Responses (1)

Write a response