One of the more confusing aspects of the useEffect() hook is the dependency array.
As a reminder, here’s the syntax for useEffect:
1useEffect(() => {}, [])
2
3
The dependency array is the second parameter, []. Whenever any of the dependencies specified in the array change, the function is re-executed.
But what does that really mean?
To understand, let's take a step back and look at Effects in React in general.
Suppose you are building a dashboard with a list of patients, and you want your List component to fetch data from an API every time the component renders (in our case, probably on something like page load or a refresh button click). In React, you would do this using an Effect. Unlike events, effects are triggered by rendering itself rather than a particular interaction like a click. Effects run at the end of the rendering process after the screen updates and are meant to help you synchronize your component to some external system.
To declare an effect in your component, you use the useEffect React hook, and call it at the top level of your component.
1import React, {useState} from "react";
2
3const PatientList = ({url}) => {
4 const [patients, setPatients] = useState("");
5
6 useEffect(() => {
7 async function fetchData() {
8 // Call fetch as usual
9 const res = await fetch(
10 Url + "/patients"
11 );
12
13 // Pull out the data as usual
14 const json = await res.json();
15
16 // Save the posts into state
17 // (look at the Network tab to see why the path is like this)
18 setPatients(json.data.children.map(c => c.data));
19 }
20
21 fetchData();
22 })
23
24 return (
25 <ul>
26 {patients.map(patient => (
27 <li key={patient.id}>{patient.name}</li>
28 ))}
29 </ul>
30 );
31};
32
33
Every time your component renders, React will update the screen and then run the code inside useEffect.
But what if you don’t want the code inside useEffect to run after every render?
For example, in the code above, the component re-renders after every state change, which means that every time `patients` changes, the code inside `useEffect` reruns. Since `patients` is changed in useEffect, this creates an infinite re-render.
You can tell React to skip unnecessarily rerunning the effect by specifying an array of dependencies as the second argument to the useEffect call. Only if one of the dependencies has changed since the last render, will the effect be rerun. If you specify an empty array, then the effect will only be run once, upon component mount.
1import React, {useState} from "react";
2
3const PatientList = ({url}) => {
4 const [patients, setPatients] = useState("");
5
6 useEffect(() => {
7 async function fetchData() {
8 // Call fetch as usual
9 const res = await fetch(
10 url + "/patients"
11 );
12
13 // Pull out the data as usual
14 const json = await res.json();
15
16 // Save the posts into state
17 // (look at the Network tab to see why the path is like this)
18 setPatients(json.data.children.map(c => c.data));
19 }
20
21 fetchData();
22
23 }, [url])
24
25 return (
26 <ul>
27 {patients.map(patient => (
28 <li key={patient.id}>{patient.name}</li>
29 ))}
30 </ul>
31 );
32};
33
In the code above, the code inside useEffect will only be rerun if the url prop changes.
Callout: The behaviors without the dependency array and with an empty [] dependency array are very different:
1useEffect(() => {
2 // This runs after every render
3});
4
5useEffect(() => {
6 // This runs only on mount (when the component appears)
7}, []);
8
You might be wondering how to go about figuring out which dependencies to include in the dependency array. Fortunately, the React team has put out a plugin, ESLint rules for React Hooks, that will automatically parse your code and let you know what to include.
With this plugin installed, if you specify an empty dependency array, but the code inside useEffect actually has a dependency, then you’ll see a lint error.
Now, let’s go through some case studies of how to use the dependency array in useEffect in real world ways.
Developers who are just starting out with useEffect often find themselves writing effects that result in infinite renders. If you find yourself in this situation, it’s good to go through the checklist below to make sure that you’ve covered all your bases.
- Make sure that you are not missing a dependency array. As noted previously in this blog post, there’s a big difference in behavior between an empty dependency array and no dependency array.
1
2useEffect(() => {
3 // This runs after every render causing infinite renders
4});
5
6useEffect(() => {
7 // This runs only on mount (when the component appears)
8}, []);
9
10
2. If you do have a dependency array, make sure that inside the useEffect, you’re not setting state variables that are also dependencies. If you are, the effect will run every time those state variables are changed, creating an infinite loop. For example:
1const [serviceList, setServiceList] = useState([]);
2var [bookButton, setBookButton] = useState(false);
3useEffect(()=>{
4 fetch('/viewall')
5 .then((response) => {
6 setServiceList(response.data);
7 serviceList.filter(function(serv){
8 if(serv.userId === userId){
9 setBookButton(false);
10 }
11 else{
12 setBookButton(true);
13 }
14 }
15 );
16
17 console.log(serviceList1);
18 })
19 }, [serviceList, bookButton]);
20
In this example, `serviceList`, and `bookButton` are state variables that are getting reset inside the effect. Every time they change, they trigger another rerun of the effect, ad nauseam. To get rid of the infinite loop, you need to remove the serviceList and bookButton from the dependency list.
1
2const [serviceList, setServiceList] = useState([]);
3var [bookButton, setBookButton] = useState(false);
4
5useEffect(()=>{
6 fetch('/viewall')
7 .then((response) => {
8 setServiceList(response.data);
9 serviceList.filter(function(serv){
10 if(serv.userId === userId){
11 setBookButton(false);
12 }
13 else{
14 setBookButton(true);
15 }
16 }
17 );
18 console.log(serviceList1);
19 })
20 }, []);
21
22
In the examples above, we have primarily passed primitive types – numbers, strings, and booleans – into the dependency array. What about objects, arrays, and functions? These complex values pose a challenge because React uses referential equality to check whether these complex values have changed.
In particular, React checks to see if the object in the current render points to the same object in the previous render. The objects have to be the exact same object in order for useEffect to skip running the effect. So even if the contents are the exact same, if a new object is created for the subsequent render, useEffect will rerun the effect.
If you have an object or array as a dependency of useEffect, and then update that object inside the effect, you effectively create a new object with a new reference and cause an infinite loop.
1// this will have an infinite loop
2useEffect(() => {
3 if (patient.name === 'jimmy') {
4 setPatient(p => ({...p, age: p.age + 1}));
5 }
6 }, [patient]);
7
The way to address this is to avoid using the entire object as a dependency: instead, you should use a specific property only.
What about if you want to pass a function to a dependency array? Consider the code below. It’s defining an async function `initializeAccount` that is calling out to several API’s and setting some state.
1const [user, setUser] = useState(null)
2const [profile, setProfile] = useState(null)
3const [posts, setPosts] = useState(null)
4
5const initializeAccount = async () => {
6 try {
7 const user = await fetch('api/user/')
8 const profile = await fetch('api/profile/')
9 const posts = await fetch('api/posts/')
10 if (user) {
11 setUser(user.data)
12 }
13 if (profile) {
14 setProfile(profile.data)
15 }
16 if (posts) {
17 setPosts(posts.data)
18 }
19 } catch (e) {
20 console.log('could not initialize account')
21 }
22}
23
24useEffect(() => {
25 initializeAccount()
26 return () => console.log('unmount')
27}, [])
28
If you run this example as written, ESLint will complain that `initializeAccount` should actually be in the dependency array.
While you can technically overwrite the linter, the linter tends to flag antipatterns that you want to avoid. It may also be the case that you’re passing the function as a prop to your component. That function can be one of several different functions. You want to make sure that if you receive a new function, you call out the effect once more.
But when you actually add `initializeAccount` to the dependency array, that creates another infinite loop
1useEffect(() => {
2 initializeAccount()
3 return () => console.log('unmount')
4}, [initializeAccount])
5
Why is this?
The issue is that similar to the object example above, upon each render cycle, `initializeAccount` is redefined and has a different reference.
The solution here is to use the useCallback hook to memoize the function so that on subsequent updates of the component, the function keeps its referential equality, and therefore does not trigger the effect.
1const initializeAccount = useCallback(async () => {
2 try {
3 const user = await fetch(url + 'api/user/')
4 const profile = await fetch(url + 'api/profile/')
5 const posts = await fetch(url + 'api/posts/')
6 if (user) {
7 setUser(user.data)
8 }
9 if (profile) {
10 setProfile(profile.data)
11 }
12 if (posts) {
13 setPosts(posts.data)
14 }
15 } catch (e) {
16 console.log('could not initialize account')
17 }
18}, [])
19
20useEffect(() => {
21 initializeAccount()
22 return () => console.log('unmount')
23}, [initializeAccount])
24
useCallback also takes its own dependency array, which you can use to pass any variables that the function depends on. For example, let’s say that the component actually takes in a url prop, which is used in the API request. In the code below, because you have added the url s a dependency in the useCallback’s dependency array, the initializeAccount function will be re-initialized every time that url changes. This, in turn, will trigger a re-rerun of the useEffect.
1const initializeAccount = useCallback(async () => {
2 try {
3 const user = await fetch(url + 'api/user/')
4 const profile = await fetch(url + 'api/profile/')
5 const posts = await fetch(url + 'api/posts/')
6 if (user) {
7 setUser(user.data)
8 }
9 if (profile) {
10 setProfile(profile.data)
11 }
12 if (posts) {
13 setPosts(posts.data)
14 }
15 } catch (e) {
16 console.log('could not initialize account')
17 }
18}, [url])
19
20useEffect(() => {
21 initializeAccount()
22 return () => console.log('unmount')
23}, [initializeAccount])
24
A common feature in many products is autosuggest in a textbox. The user starts typing in a textbox and a dropdown menu suggests a bunch of phrases. You can use useEffect to build this feature.
In the code below, the useEffect filters for suggestions that contain what has been typed in the input box. `inputValue` is a dependency in the useEffect dependency array, which means that every time `inputValue` is updated (every time that a new character is typed into the input box), then the effect will rerun.
1import React, { useEffect, useState } from "react";
2
3const array = [
4 'change', 'challenge', 'charles'
5];
6
7const AutoCompleteInput = () => {
8 const [inputValue, setInputValue] = useState('');
9 const [filteredArray, setFilteredArray] = useState(array);
10
11 const inputValueHandler = e => {
12 setInputValue(e.target.value);
13 };
14
15 useEffect(() => {
16 setFilteredArray((_) => {
17 const newArray = array.filter(item => item.includes(inputValue));
18 return newArray;
19 });
20 }, [inputValue]);
21
22 return (
23 <div>
24 <input type="text" id="input-value" onChange={inputValueHandler} />
25 </div>
26 )
27
28};
29
30export default AutoCompleteInput;
31
32
Let’s say that you have an effect that uses a particular value. ESLint says that you have to include this value in the dependency array, but you don’t actually want to rerun the effect when that value changes. For example, let’s consider the code below where we want different functions to run depending on the size of the screen.
1const patientList = ({
2 isDesktop,
3 selectedPatient
4}) => {
5
6 useEffect(() => {
7 isDesktop ? showPatientModal() : showPatientPage()
8 }, [selectedPatient])
9
10 return (
11 <div className="App">
12 <h1>{selectedPatient.name}</h1>
13 <p>{selectedPatient.birthday}</p>
14 </div>
15 );
16}
17
Let’s say in this example that you don’t actually want to rerun the effect when the browser window changes size. You only want to rerun the effect when the `selectedPatient` changes. But the linter complains that `isDesktop` is not in the dependency array.
The correct solution here is to use `isDesktop` and `selectedPatient` to initialize component state variables. Whenever `selectedPatient` changes, the first useEffect will update both states with the passed-in props, and trigger the second useEffect() to rerun on the next render. By refactoring the code this way, you are able to remove isDesktop from the main effect itself, and not have to include it in the dependency array while keeping the linter happy.
1const patientList = ({
2 isDesktop,
3 selectedPatient
4}) => {
5 const [desktop, setDesktop] = useState(isDesktop)
6 const [patient, setPatient] = useState(selectedPatient)
7
8 useEffect(() => {
9 if (patient !== selectedPatient) {
10 setDesktop(isDesktop)
11 setPatient(selectedPatient)
12 }
13 }, [isDesktop, selectedPatient, patient])
14
15 useEffect(() => {
16 if (desktop) showPatientModal()
17 Else showPatientPage()
18 }, [desktop, patient])
19
20 return (
21 <div className="App">
22 <h1>{selectedPatient.name}</h1>
23 <p>{selectedPatient.birthday}</p>
24 </div>
25 )
26}
27
The dependency array in useEffect can be confusing and as a result its nuances are often disregarded. From the examples above, though, you can see that it plays a major role in regulating the state management of React components and writing clean, “pure” React code.
Reader