Understanding Referential Equality in React

Understanding Referential Equality in React

What is referential equality? Why it is useful to understand, and how referential equality affects component re-rendering in React

In React, state variables are used to render the component state on the browser. When the state changes, React re-renders the component with the new data. It helps keep the user informed of all changes occurring in the application.

However, many React developers do not manage the state variables properly. This results in re-renders not occurring despite the state changing. It will then make an awful user experience in the application.

Therefore, this article will explain how React re-renders a component and what we can do to ensure that the state changes get reflected in the UI smoothly.

How Does React Determine a Component Re-Render?

React re-renders a component only when a change occurs to its state variables or props.

This is shown in the example given below:

const [name, setName] = useState('');
const onButtonClick = (newName: string)=> {
   setName(newName);
};

The code snippet displayed above has a method named onButtonClick that updates the state variable - name with a new value.

React compares the new value against the old value for equality using the Object.is() comparison algorithm whenever a new value is assigned to name variable.

If the values are equal, React bails out the re-render. But if the values are different, React triggers a component re-render to reflect the state change.

The Comparison Algorithm — Object.is()

The Object.is() algorithm determines whether two values are the same if:

  1. Both values are undefined or null.
  2. Both values are either true or false.
  3. Both values are Strings having the same characters, length, and order.
  4. Both values are Numbers with the same value or NaN.
  5. Both values are Objects that point to one memory location.

React applies these rules to re-render components whenever a state change is made.

The Comparison Algorithm in Action — Referential Equality

Consider the code shown below to identify how developers can introduce bugs with state management using React to understand Object.is() better.

import { useState } from 'react';
import './App.css';

const App = () => {
  const [mySelf, setMySelf] = useState<{ name: string, age: number }>({ name: 'David', age: 30 });

  const changeNameToJohn = () => {
    mySelf.name = "John";
    mySelf.age = 30;
    console.log(mySelf);
    setMySelf(mySelf)
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col-sm-12" style={{ textAlign: 'center', padding: 30 }}>
          <p>My name is {mySelf.name} and I am {mySelf.age} years of age.</p>
          <button className="btn btn-primary" onClick={changeNameToJohn}>Change My Name To John</button>
        </div>
      </div>
    </div>
  );
}

export default App;

The code snippet above shows a state variable named mySelf initialized as an object with properties name and age set to "David" and "30", respectively. Furthermore, an event listener named changeNameToJohn is declared to change the name to a new value, "John."

The output of this code is shown below:

Figure 2. Expected output of code snippet

When the button clicks, the name should change to “John” and trigger a re-render to update the UI.

But, currently, it does not happen even-though logging the object displays the updated values, as shown below:

Figure 3. Observing the console after the button gets clicked

Many of you may wonder why the component does not get re-rendered. Looking at the fifth principle of the Object.is() algorithm may help you understand this situation.

Two values are considered equal when both are objects that point to the exact memory location.

With the help of this principle, the bug in the above code can be identified.

const changeNameToJohn = () => {
   mySelf.name = "John";
   mySelf.age = 30;
   console.log(mySelf);
   setMySelf(mySelf);
};

During the state change shown above, it assigns the new name to the property of the current object. When the comparison algorithm gets applied by React, the new and current values will still be equal because, behind the scenes, the two values point to the exact memory location.

Therefore, React classifies these values as equal and does not trigger a re-render. Hence, the UI does not reflect the state change.

This process is called Referential Equality, for objects are considered equal based on their memory location and not the values.

Problems with the Referential Equality

Ignoring the Referential Equality introduces minor bugs as discussed above. But, when developing React components, we use effects (useEffect) and memorized callbacks (useCallback) that get triggered only when a value in their dependency array changes.

One minor state management error creates numerous bugs throughout the component in situations like this.

For example, consider the code snippet shown below:

import { useCallback, useEffect, useState } from 'react';
import './App.css';

const App = () => {
  const [mySelf, setMySelf] = useState<{ name: string, age: number }>({ name: 'David', age: 30 });
  const [header, setHeader] = useState<string>('Hello David!');

  const changeNameToJohn = () => {
    mySelf.name = "John";
    mySelf.age = 30;
    console.log(mySelf);
    setMySelf(mySelf);
  }
  const constructTheNameChangeMessage = useCallback(() => {
    if (mySelf) {
      setHeader(`Hello ${mySelf.name}!`);
    }
  }, [mySelf]);

  useEffect(() => {
    constructTheNameChangeMessage();
  }, [constructTheNameChangeMessage]);

  return (
    <div className="container">
      <div className="row">
        <div className="col-sm-12" style={{ textAlign: 'center', padding: 30 }}>
          <h1>{header}</h1>
          <p>My name is {mySelf.name} and I am {mySelf.age} years of age.</p>
          <button className="btn btn-primary" onClick={changeNameToJohn}>Change My Name To John</button>
        </div>
      </div>
    </div>
  );
}
export default App;

Figure 4 shows an updated version of the first example (figure 1). It contains an effect that executes a method constructTheNameChangeMessage(). It gets wrapped in a callback to return memorized output that changes only when the object mySelf changes (as declared in the dependency array).

The header should change its value when the button clicks, but it doesn’t.

It occurs because of Referential Equality. React applies the Object.is() algorithm and classifies the present mySelf equal with the new value. Therefore, the method - constructTheNameChangeMessage does not return a new memorized version as the values in its dependency array remains unchanged. As a result, the effect does not get executed because the memorized callback does not change.

It introduces a chain of bugs in the code that can negatively affect the user experience. Therefore, it is crucial to address this issue to re-render React components as expected.

Using Referential Equality with React

According to the principle of Object.is(), any two objects pointing to the exact memory location are considered equal. Considering this, implementing the fix is straightforward.

Consider the fix shown below:

const changeNameToJohn = () => {
   setMySelf({ ...mySelf, name: 'John' });
}

When the previous code gets replaced with the updated code shown above, we see the output below once the button clicks.

Figure 5. Output of the updated code

Figure 5 shows the state updating and re-rendering successfully as the new state update gets reflected in the UI.

But, what has changed?

When the new event handler is observed, the object — { ...mySelf, name: 'John'} gets passed directly into the method - setMySelf to update the state. By doing so, a new object gets passed into the method rather than updating the existing object.

Therefore, when React applies the Object.is() algorithm, the value that gets passed is not equal to the current value stored in mySelf because the new object points to another memory location. As a result, it causes the state to get updated, creating a chain reaction within the React ecosystem.

This state update will:

  1. Make React return a new memorized output of constructTheNameChangeMessage because the values declared in its dependency array change.
  2. Cause React to execute the effect -useEffect as the callback returned a new memorized output.
  3. Re-render the component to reflect the new state.

Since the state gets successfully rendered on the user interface, it helps keep the user informed of all changes in the application allowing it to function as expected.

Source https://blog.bitsrc.io/understanding-referential-equality-in-react-a8fb3769be0

JAMSTACK is
Awesome

Obsessed with Technology.

This site is built on JAMStack architecture:
GhostJS as headless CMS & content API,
GatsbyJS for Static Site Generation (SSG ), GitHub Actions for CI/CD.
NodeJS , ReactJS & GraphQL

© 2023 — Mursaleen