Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 5, 2022 01:44 am GMT

Understanding referential equality in React's useEffect

Hello fellow readers!
In this post I am going to discuss how useEffect handles it's dependencies when there is an object in it.

Note: there will be assumptions that you know some key concepts about useEffect. So, if you don't really know the basics, I first recommend you to read the React docs on this subject.

Referential equality

When we talk about comparison in most of programming languages, we deal with two topics: comparison by reference and comparison by value.
In JavaScript world, this is also true. We can compare values using primitive types, like string or numbers, or compare references when dealing with objects.

Comparison by value

This is the most straightforward concept. If two values are equal, then a boolean comparison returns true. Note that this works for the most common primitive types of JavaScript (strings, numbers and booleans).

const a = 1;const b = 1;const c = 2;console.log(a === b); // trueconsole.log(b === c); // falseconst d = 'hello';const e = 'hello';const f = 'bye';console.log(d === e); // trueconsole.log(e === f); // false

Comparison by reference

This type of comparison takes in consideration where in the memory an object is located. If two objects point to the same location, they are equal, otherwise they are different. Check out the following schema:

Drawing showing two objects pointing to two different memory pieces

Even if two objects have the same properties with the same values, they will not be equal, unless they are located in the same memory position. You can run the following code in your browser's DevTools to prove this:

const obj1 = { animal: 'dog' };const obj2 = { animal: 'dog' };const obj3 = obj1console.log(obj1 === obj1) // trueconsole.log(obj1 === obj2) // falseconsole.log(obj2 === obj3) // falseconsole.log(obj1 === obj3) // true

Comparison in React's useEffect

With the previous introduction on types of comparison on mind, let's bring that concept into React's hook useEffect.
Accordingly to React's docs, we can define this hook as:

The Effect Hook lets you perform side effects in function components. Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects. Whether or not youre used to calling these operations side effects (or just effects), youve likely performed them in your components before.

If we need to run an effect after a specific change, we must use hook's second argument, which is an array of dependencies:

useEffect(() => {  document.title = `You clicked ${count} times`;}, [count]); // Only re-run the effect if count changes

Every time any of the dependencies change, the callback inside useEffect is run, and in this process it is important to know how the comparison is made.

If there are only primitive values such as string or number, there will be a comparison by value, otherwise there will be a comparison by reference.

I've seen plenty of times errors regarding the functionality of useEffect when it comes to dependencies. You may trap yourself in an infinite loop or multiple calls to an API, which may incur in money loss if, for example, your back-end is hosted in a cloud service. To mitigate these problems, it is important to keep these dependencies as stable as possible.

So, let's see some examples.

  • useEffect + value comparison: this example show a simple count component that render in screen a new text every time the count state changes. As it is a number, React simply compares if the previous number is equal the new number and, if this is true, then useEffect is called.
const ValueComparison = () => {  const [count, setCount] = useState(0);  useEffect(() => {    document.body.append(`Whoa! My count is now: ${count}`);    var br = document.createElement('br');    document.body.appendChild(br);  }, [count]);  return <button onClick={() => setCount(count + 1)}>Click me to count</button>;};

Gif showing a button that when clicked increments a counter and renders in screen a text with the current count

  • useEffect + reference comparison (1): the following example shows a common problem. It shows a object state that is directly changed, but nothing is rendered. Check it out:
const ReferenceComparison1 = () => {  const [animalObj, setAnimalObj] = useState({ animal: 'dog' });  const handleChange = () => {    animalObj.animal = animalObj.animal === 'cat' ? 'dog' : 'cat';    setAnimalObj(animalObj);  };  useEffect(() => {    document.body.append(`I am this animal: ${animalObj.animal}`);    var br = document.createElement('br');    document.body.appendChild(br);  }, [animalObj]);  return <button onClick={handleChange}>Click me to change the animal</button>;};

Gif showing a non working button that when clicked does nothing

You may be asking yourself, baffled: but the state did change! Now the animal should be a cat!
Well... not quite. We are changing an object property, not the object per se. See, remember that an object comparison is made by reference? So, the reference of the object in the memory stays the same even if some property changes, thereby useEffect dependency will not recognize any change.

To fix this, we simply need to pass a new object to setAnimalObj, meaning that this new object will point to a new memory location, so the dependency will change and useEffect will fire:

const ReferenceComparison1 = () => {  const [animalObj, setAnimalObj] = useState({ animal: 'dog' });  const handleChange = () => {    setAnimalObj({      ...animalObj,      animal: animalObj.animal === 'cat' ? 'dog' : 'cat',    });  };  useEffect(() => {    document.body.append(`I am this animal: ${animalObj.animal}`);    var br = document.createElement('br');    document.body.appendChild(br);  }, [animalObj]);  return <button onClick={handleChange}>Click me to change the animal</button>;};

Gif showing a button that when clicked renders in screen wether the current animal is a dog or a cat

  • useEffect + reference comparison (2): now let's see an example with a parent-child component relationship:
// Here is the parent component that renders an animal list and a button that increments a counterconst ReferenceComparison2 = () => {  const [count, setCount] = useState(0);  const animalList = [    { animal: 'dog' },    { animal: 'cat' },    { animal: 'turtle' },  ];  return (    <React.Fragment>      <ChildComponent data={animalList} />      <span>Count: {count}</span>      <button onClick={() => setCount(count + 1)}>Increment count</button>    </React.Fragment>  );};// Here is the child component, responsible for rendering the list used by parent componentconst ChildComponent = ({ data }: ChildComponent1Props) => {  useEffect(() => {    document.body.append(`Child rendered! Data has changed!`);    var br = document.createElement('br');    document.body.appendChild(br);  }, [data]);  return (    <ul>      {data.map((item, index) => (        <li key={index}>{item.animal}</li>      ))}    </ul>  );};

If we run the above code, we can see that the child component is rerendered every time the button is clicked, although the counter and the list are independent:

Gif showing an animal list with a button. Below these, there is a text showing how many times the child component was rendered

This happens because every time the counter is updated, the parent component is rerendered, therefore the function will be called again, generating a new reference for the object in animalList variable. Finally, the child component acknowledge this change and runs useEffect.

Drawing representing that the animalList variable points to different memory blocks on each render

It is possible to solve this in many ways, let's see two of them. The first solution below simply moves the array data outside the component function, therefore the object reference will never change:

const animalList = [{ animal: 'dog' }, { animal: 'cat' }, { animal: 'turtle' }];const ReferenceComparison2 = () => {  const [count, setCount] = useState(0);  return (    <React.Fragment>      <ChildComponent data={animalList} />      <span>Count: {count}</span>      <button onClick={() => setCount(count + 1)}>Increment count</button>    </React.Fragment>  );};

The second possible solution is to use useMemo. This hook keeps the same reference of a value unless its dependencies change:

const ReferenceComparison2 = () => {  const [count, setCount] = useState(0);  const animalList = useMemo(    () => [{ animal: 'dog' }, { animal: 'cat' }, { animal: 'turtle' }],    []  );  return (    <React.Fragment>      <ChildComponent data={animalList} />      <span>Count: {count}</span>      <button onClick={() => setCount(count + 1)}>Increment count</button>    </React.Fragment>  );};

Now our child component will not run useEffect, because the data dependency has a stable reference:

Gif showing that now, when the increment counter button is clicked, the child component does not rerender

Wrapping up

We've seen how referential equality works when using useEffect. It is always important to keep an eye on the dependencies, specially if they rely on objects, arrays or functions.
You may find yourself sometimes in trouble when the same effect runs many times. If this happens, remember to checkout the dependencies and if they are stable.
Feel free to use the comments section to expose your opinion or ask me anything! Thanks!


Original Link: https://dev.to/vicnovais/understanding-referential-equality-in-reacts-useeffect-2m7o

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To