React useEffect — Gotchas

This post assumes you are a React developer and has used useEffect in your code. But you have observed the behavior of useEffect is weird in certain scenarios and you need to know Why !

I am not going to discuss on what a useEffect is and how you write a simple useEffect. I will focus on describing common mistakes we do when using useEffect and help you to not do the same mistake again.

Alrighty ! Let’s talk about important facts that we need to keep in mind when working with useEffect in React.

  1. useEffect run after your component renders

You can see the order of console.log statements. The component will render first and React will update the screen. Then only the code inside useEffect is executed.

     2.  By default useEffect run after every render

This behavior can result in infinite loops, if not used without a proper understanding.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { useEffect, useState } from 'react';
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
setCounter(prev => prev + 1);
})
return (
<div className="App">
<p>Counter {counter}</p>
</div>
);
}
export default App;
import { useEffect, useState } from 'react'; function App() { const [counter, setCounter] = useState(0); useEffect(() => { setCounter(prev => prev + 1); }) return ( <div className="App"> <p>Counter {counter}</p> </div> ); } export default App;
import { useEffect, useState } from 'react';

function App() {
  const [counter, setCounter] = useState(0);
  useEffect(() => {
    setCounter(prev => prev + 1);
  })
  return ( 
    <div className="App">
      <p>Counter  {counter}</p>
    </div>
  );
}

export default App;

In the above example, useEffect will run after the component renders. Inside useEffect we change state of count variable. Change of state causes the component to re-render.  Once the component re-renders useEffect will run again. This repeating process will result in an infinite loop.

   3. Specify dependencies in your useEffect, but keep in mind you cannot choose the dependencies of your Effect

 

If we do not need to run useEffect with each re-render we can add an empty array as a dependency.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { useEffect, useState } from 'react';
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
console.log('This useEffect will only run once');
setCounter(prev => prev + 1);
}, [])
return (
<div className="App">
<p>Counter {counter}</p>
</div>
);
}
export default App;
import { useEffect, useState } from 'react'; function App() { const [counter, setCounter] = useState(0); useEffect(() => { console.log('This useEffect will only run once'); setCounter(prev => prev + 1); }, []) return ( <div className="App"> <p>Counter {counter}</p> </div> ); } export default App;
import { useEffect, useState } from 'react';

function App() {
  const [counter, setCounter] = useState(0);
  useEffect(() => {
    console.log('This useEffect will only run once');
    setCounter(prev => prev + 1);
  }, [])
  return ( 
    <div className="App">
      <p>Counter  {counter}</p>
    </div>
  );
}

export default App;

The important thing here is, we cannot choose the dependencies of useEffect. Every reactive value used in effect needs to be declared as a dependency. Reactive values include props and all variables and functions declared directly inside of your component.

 

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { useEffect, useState } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
if (count > 0) {
console.log("Greater than zero");
}
}, []);
return (
<div className="App">
<p>Counter example {count}</p>
</div>
);
}
export default App;
import { useEffect, useState } from "react"; function App() { const [count, setCount] = useState(0); useEffect(() => { if (count > 0) { console.log("Greater than zero"); } }, []); return ( <div className="App"> <p>Counter example {count}</p> </div> ); } export default App;
import { useEffect, useState } from "react";
function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (count > 0) {
      console.log("Greater than zero");
    }
  }, []);
  return (
    <div className="App">
      <p>Counter example {count}</p>
    </div>
  );
}

export default App;

 

The above code gives us the warning that React Hook useEffect has a missing dependency: ‘count’. Either include it or remove the dependency array react-hooks/exhaustive-deps.

Your useEffect uses count variable, you cannot remove it from your dependency list. Your Effect’s dependency list is determined by the surrounding code. If you do not wish to include count in your dependency list, you have to make it to be a NOT a reactive value.

 

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { useEffect } from "react";
function App() {
const count = 20; // this is not a reactive value
useEffect(() => {
if (count > 0) {
console.log("Greater than zero");
}
}, []);
return (
<div className="App">
<p>Counter example {count}</p>
</div>
);
}
import { useEffect } from "react"; function App() { const count = 20; // this is not a reactive value useEffect(() => { if (count > 0) { console.log("Greater than zero"); } }, []); return ( <div className="App"> <p>Counter example {count}</p> </div> ); }
import { useEffect } from "react";
function App() {
  const count = 20; // this is not a reactive value

  useEffect(() => {
    if (count > 0) {
      console.log("Greater than zero");
    }
  }, []);
  return (
    <div className="App">
      <p>Counter example {count}</p>
    </div>
  );
}

 

We have assigned a constant value to count variable and it is no longer a reactive value. Therefore we do not get missing dependency warning now 😎. If your Effect’s code doesn’t use any reactive values, its dependency list should be empty ([])

4. Primitive vs Non Primitive Dependencies

The dependencies that we include in useEffect can be either Primitive Type or Reference Type. Primitive types are number, string, boolean, null or undefined. Reference types contain objects, arrays or functions. React will compare each dependency with its previous value using Object.is

You will often come across issues when using reference types in your dependency list. Let’s talk more about how to use reference types properly in your dependency list. (I will not provide an example for primitive types as usage of it is very straightforward and you will not have any issue when using primitive types in your effects dependency list)

 

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { useEffect, useState } from "react";
function App() {
const [user, setUser] = useState({
name: 'Gihan',
email: 'test@example.com'
})
useEffect(() => {
console.log("Use effect runs");
}, [user]);
const changeUser = () => {
setUser({
name: 'Gihan',
email: 'test@example.com'
})
}
return (
<div>
<p>Hi {user.name} {user.email}</p>
<button onClick={changeUser}>Change User</button>
</div>
);
}
export default App;
import { useEffect, useState } from "react"; function App() { const [user, setUser] = useState({ name: 'Gihan', email: 'test@example.com' }) useEffect(() => { console.log("Use effect runs"); }, [user]); const changeUser = () => { setUser({ name: 'Gihan', email: 'test@example.com' }) } return ( <div> <p>Hi {user.name} {user.email}</p> <button onClick={changeUser}>Change User</button> </div> ); } export default App;
import { useEffect, useState } from "react";
function App() {
  const [user, setUser] = useState({
    name: 'Gihan',
    email: 'test@example.com' 
  })
  useEffect(() => {
    console.log("Use effect runs");
  }, [user]);

  const changeUser = () => {
    setUser({
      name: 'Gihan',
      email: 'test@example.com' 
    })
  }
  return (
    <div>
      <p>Hi {user.name} {user.email}</p>
      <button onClick={changeUser}>Change User</button>
    </div>
  );
}

export default App;

 

We have initialised user state to be { name: ‘Gihan’ , email :’test@example.com’ }. There is a click handler to change user, but changeUser method sets user state to same old value. We have used user object as a dependency in useEffect. You can see component re-renders even though the values of user properties were set to previous values. If this is a primitive type, there will be no component re-render as the new value is still same as previous value. But in objects, arrays or functions we are comparing the reference. Even though the actual values were not changed, object reference gets changed in this assignment. Therefore React will re-render the component

We can avoid this unnecessary re-rendering when we use reference types 

  • Do not use the whole object, use individual properties of the object in your dependency list

 

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
useEffect(() => {
console.log("Use effect runs");
}, [user.name, user.email]);
useEffect(() => { console.log("Use effect runs"); }, [user.name, user.email]);
useEffect(() => {
  console.log("Use effect runs");
}, [user.name, user.email]);
  • You can use JSON.stringify on user object, so content will always be compared

 

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
useEffect(() => {
console.log("Use effect runs");
}, [JSON.stringify(user)]);
useEffect(() => { console.log("Use effect runs"); }, [JSON.stringify(user)]);
useEffect(() => {
  console.log("Use effect runs");
}, [JSON.stringify(user)]);

 

5. useEffect cleanup

 

React state update on an unmount component

When you work with React you may have seen this issue and might be wondering what is the root cause for this issue. I took this image from one of the questions in StackOverflow.

Imagine you have two React components in your application. Inside the first component you try to fetch some data from a third party API and update the state, but before the promise is resolved you navigate to the next component. When you navigate from your component is gets unmounted, but the component is still fetching data. When the data fetching is complete, it tries to update the state, but you are no longer in this component and it is unmounted. Because you navigated to a different component. This is the reason for the above warning. You can avoid this warning by using a cleanup function in your useEffect. This cleanup function will be executed before your component gets unmounted thus avoiding unnecessary memory leaks.

 

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { useEffect, useState } from "react";
function MyPosts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
console.log("UseEffect runs");
fetch("https://jsonplaceholder.typicode.com/posts")
.then((res) => res.json())
.then((data) => {
setPosts(data);
});
}, []);
return (
<div>
{posts.map((post) => (
<div key={post.id}>
<p>{post.title}</p>
</div>
))}
</div>
);
}
export default MyPosts;
import { useEffect, useState } from "react"; function MyPosts() { const [posts, setPosts] = useState([]); useEffect(() => { console.log("UseEffect runs"); fetch("https://jsonplaceholder.typicode.com/posts") .then((res) => res.json()) .then((data) => { setPosts(data); }); }, []); return ( <div> {posts.map((post) => ( <div key={post.id}> <p>{post.title}</p> </div> ))} </div> ); } export default MyPosts;
import { useEffect, useState } from "react";
function MyPosts() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    console.log("UseEffect runs");
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((res) => res.json())
      .then((data) => {
        setPosts(data);
      });
  }, []);
  return (
    <div>
      {posts.map((post) => (
        <div key={post.id}>
          <p>{post.title}</p>
        </div>
      ))}
    </div>
  );
}

export default MyPosts;

 

Once MyPosts component renders, useEffect will make a http call to a third party API to get some fake posts. Once the promise is resolved we update posts state (using setPosts) with the data. There is no issue in the above code example, but you will encounter an error if you unmount this component before data fetching completes. Let’s see how we can improve the above code example using a cleanup function

 

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
useEffect(() => {
// AbortController is javaScript interface which allow you to abort a web request when desired
const controller = new AbortController();
const signal = controller.signal;
console.log("UseEffect runs");
fetch("https://jsonplaceholder.typicode.com/posts", { signal})
.then((res) => res.json())
.then((data) => {
setPosts(data);
});
// Cleanup function
return () => {
console.log('Cleaning up useEffect');
controller.abort();
}
}, []);
useEffect(() => { // AbortController is javaScript interface which allow you to abort a web request when desired const controller = new AbortController(); const signal = controller.signal; console.log("UseEffect runs"); fetch("https://jsonplaceholder.typicode.com/posts", { signal}) .then((res) => res.json()) .then((data) => { setPosts(data); }); // Cleanup function return () => { console.log('Cleaning up useEffect'); controller.abort(); } }, []);
useEffect(() => {
  // AbortController is javaScript interface which allow you to abort a web request when desired
  const controller = new AbortController();
  const signal = controller.signal;
  console.log("UseEffect runs");
  fetch("https://jsonplaceholder.typicode.com/posts", { signal})
    .then((res) => res.json())
    .then((data) => {
      setPosts(data);
    });
  
  // Cleanup function
  return () => {
    console.log('Cleaning up useEffect');
    controller.abort();
  }
}, []);

 

We use AbortController which is an interface provided by javaScript to cancel a web request when desired. It has nothing to do with React. We pass a signal along with fetch request. Then comes our cleanup function. Inside useEffect cleanup, we abort our request. So if our component unmounts, the request will be cancelled.

 

In this post we discussed how we can avoid common mistakes when using useEffect. I hope this post will help you to write clean code.

Thanks for reading and your comments are highly appreciated.

Leave a Reply

Your email address will not be published. Required fields are marked *