Demystifying useEffect, why did it render?

Terchilă Marian
5 min readFeb 6, 2022

💡 This is a comprehensive guide meant to demystify the behaviour of the useEffect hook. This guide is addressed for both react starters and advanced users. This guide does not require prior experience with useHooks, we will cover everything as we go starting with the basics then gradually increasing the difficulty.

What is useEffect?

The Effect Hook lets you perform side effects in function components:

import React, { useState, useEffect } from 'react';function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

This snippet is based on the counter example from the previous page, but we added a new feature to it: we set the document title to a custom message including the number of clicks.

Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects. Whether or not you’re used to calling these operations “side effects” (or just “effects”), you’ve likely performed them in your components before.

React documentation

I highly recommend reading the react documentation as they do provide a great explanation of the hook.

How to prevent Infinite rendering

Let’s set up an example for a react application that displays the user object as a string, where the initial state of the user is null and we fetch the data from an external API. We want to display the user object in <pre> tags in the application or User unknown if the user is null. I’ve added two console logs to help us understand the behaviour of the hook and when the render is triggered.

How many times will the snippet below render the page?

import {useState, useEffect} from "react";

const App = () => {
const [user, setUser] = useState(null)
console.log('Rendering App');

useEffect(() => {
console.log('Running useEffect');
fetch('<https://jsonplaceholder.typicode.com/users/1>')
.then(response => response.json())
.then(json => setUser(json))
})

return (
<div>
{user ?
<>
<pre>{JSON.stringify(user)}</pre>
</>
:
<p>User unknown</p>
}

</div>
);
}

export default App;

The answer to that is forever! But why?!? Why does it rerender the application endlessly?

Let’s have a look and see how our code gets executed.

  1. Initially, we start with the user state set to null . This is what we defined as our initial state.
  2. Next, our useEffect fetches the data from the external API and updates it using setUser then we enter a loop that we cannot get out of constantly re-rendering the application.

One of the reasons for that is that we haven’t specified a dependency array to our useEffect.

The default behaviour for effects is to fire the effect after every completed render. That way an effect is always recreated if one of its dependencies changes.

To implement this, pass a second argument to useEffect that is the array of values that the effect depends on. Our updated example now looks like this:

React documentation

In the end, after adding the dependency array, with the user as part of the array the code should look like this.

import {useState, useEffect} from "react";

const App = () => {
const [user, setUser] = useState(null)
console.log('Rendering App');

useEffect(() => {
console.log('Running useEffect');
fetch('<https://jsonplaceholder.typicode.com/users/1>')
.then(response => response.json())
.then(json => setUser(json))
}, [user])

return (
<div>
{user ?
<>
<pre>{JSON.stringify(user)}</pre>
</>
:
<p>User unknown</p>
}

</div>
);
}

export default App;

But guess what? It still re-renders endlessly. So, what’s going on here? How come in the react documentation, in the counterexample from the introduction section we don’t get to deal with this? It should work, right???

To answer that question we first need to understand javascript references and how they work. This is a crucial step to understand how useEffect, useMemo and the other React concepts work.

Terminology:

  • scalar — a single value or unit of data (e.g. integer, boolean, string)
  • compound — comprised of multiple values (e.g. array, object, set)
  • primitive — a direct value, as opposed to a reference to something that contains the real value.

In JavaScript scalars are primitives.

Source: Medium

Those are all scalars, evaluated and compared by value.

This comparison mechanism does not work when evaluating compound variables such as objects and arrays, which are evaluated by reference.

As you can clearly see in the image above, reference1 and reference2 are both arrays that hold the exact same string as their sole element. Then why doesn’t reference1 === reference2 ? Why is it false? Because arrays and objects are evaluated by reference.

If we create a new constant, reference3 and give it the value of the reference1 we actually point the reference3 instance to the memory allocated to the reference1, therefore making them equal.

Source: Jack Herrington

Explanation

This is why our react snippet enters that endless loop. Because altho we set up the user in our useEffect to be the one we’re grabbing from the API request, that will trigger a rendering cycle where the useEffect will run again comparing the current user in our state to the one we fetch from our API. Since the objects are compared by reference, the application will enter an infinite loop.

How can we fix this?

There are multiple ways around fixing this, but so far the easiest would be stringifying the object.

As explained previously, strings are scalars, therefore if we convert our user to be a stringify version of the object that should solve the problem.

import {useState, useEffect, useRef} from "react";

const App = () => {
const [user, setUser] = useState(null)
console.log('Rendering App');

useEffect(() => {
console.log('Running useEffect');
fetch('<https://jsonplaceholder.typicode.com/users/1>')
.then(response => response.json())
.then(json => {
setUser(JSON.stringify(json))
})
}, [user])

return (
<div>
{user ?
<>
<pre>{user}</pre>
</>
:
<p>User unknown</p>
}

</div>
);
}

export default App;

If you made it will here congrats, hope you’ve got a better understanding of the useEffect hook and that everything now makes more sense.

--

--