Preventing infinite re-renders when using useEffect and useState

Let's take a step back, pause for a moment, and think about what useEffect and useState actually do.

Changing state will always cause a re-render. By default, useEffect always runs after render has run. This means if you don't include a dependency array when using useEffect to fetch data, and use useState to display it, you will always trigger another render after useEffect runs.

Unless you provide useEffect a dependency array.

The dependency array

The dependency array in useEffect lets you specify the conditions to trigger it. If you provide useEffect an empty dependency array, it'll run exactly once, as in this example (CodeSandbox link):

import React, { useEffect, useState } from 'react';
export default function DataDisplayer() {
const [data, setData] = useState('');
useEffect(() => {
const getData = async () => {
const response = await fetch(`https://swapi.dev/api/people/1/`);
const newData = await response.json();
setData(newData);
};
getData();
}, []); //<-- This is the dependency array
if (data) {
return <div>{data.name}</div>;
} else {
return null;
}
}

What if you wanted to let users decide which id they wanted to query, and forgot to add the dependency array? You'd cause an infinite loop.

import React, { useEffect, useState } from 'react';
export default function DataDisplayer(props) {
const [data, setData] = useState('');
useEffect(() => {
const getData = async () => {
const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
const newData = await response.json();
setData(newData);
};
getData();
}); //<-- Notice the missing dependency array
if (data) {
return <div>{data.name}</div>;
} else {
return null;
}
}

Why does the above example cause an infinite loop?

  1. Your first render runs, and because data is falsey, render returns null and kicks off useEffect
  2. useEffect runs, fetching your data, and updating it via setData
  3. Since data has been updated, the component re-renders to display the new data value
  4. However, useEffect runs after each render, so it runs again, updating data via setData
  5. Repeat steps 3 and 4 until your app crashes, or the API rate-limits your requests

Preventing infinite loops

This is where the dependency array comes in handy.

Adding variables to the dependency array tells useEffect: "Hey, I need you to run only if this value changes".

In this case, adding props.id will ensure useEffect only runs if props.id changes:

import React, { useEffect, useState } from 'react';
export default function DataDisplayer(props) {
const [data, setData] = useState('');
useEffect(() => {
const getData = async () => {
const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
const newData = await response.json();
setData(newData);
};
getData();
}, [props.id]); //<-- This is the dependency array, with a variable
if (data) {
return <div>{data.name}</div>;
} else {
return null;
}
}

(Shameless plug for the useEffect book I wrote below)

Tired of infinite re-renders when using useEffect?

A few years ago when I worked at Atlassian, a useEffect bug I wrote took down part of Jira for roughly one hour.

Knowing thousands of customers can't work because of a bug you wrote is a terrible feeling. To save others from making the same mistakes, I wrote a single resource that answers all of your questions about useEffect, after teaching it here on my blog for the last couple of years. It's packed with examples to get you confident writing and refactoring your useEffect code.

In a single afternoon, you'll learn how to fetch data with useEffect, how to use the dependency array, even how to prevent infinite re-renders with useCallback.

Master useEffect, in a single afternoon.

useEffect By Example's book cover