Common useEffect mistakes
Reviewed
From version 16.8 of react, we have had the honours of using hooks to lighten up our codebase, and bring state and lifecycle methods only to the components that require it, making the class component structure not needed anymore and allowing us to use and reuse the prepacked hooks as well as the ones we make ourselves :D
While today we're not talking in details about what is a Hook, which you can find in React official documentation website (Opens in a new tab), we will spend a bit more time discussing about the common misuses of the ever so popular useEffect hook that most of us know and love for its versatility.
The useEffect hook is a feature of React that allows us to perform side effects in functional components. This can include things like modifying the DOM, fetching data, or subscribing to event listeners.
In JavaScript, a side effect is like an action that changes something outside its own function. Think of it as altering a global setting, updating a web page element, or requesting data online.
The useEffect hook in React gives you a simplified way to manage these actions in function components, bypassing the need for class-based components, simplifying overall the writing and understanding of the code and also boosting the app performance by dodging needless re-renders.
It also allows you to specify a cleanup function, which is called when the component is unmounted, to prevent memory leaks and other issues.
1import React, { useState, useEffect } from 'react'; 2 3function Example() { 4 const [count, setCount] = useState(0); 5 6 // Similar to componentDidMount and componentDidUpdate: 7 useEffect(() => { 8 // Update the document title using the browser API 9 document.title = `You clicked ${count} times`; 10 }); 11 12 return ( 13 <div> 14 <p>You clicked {count} times</p> 15 <button onClick={() => setCount(count + 1)}> 16 Click me 17 </button> 18 </div> 19 ); 20}
There are several common mistakes that developers make when using the useEffect hook in React. Between newsfeeds, videos and personal experiences, I've seen a few cases more often than others; let's go through some of those together :D
1. Failing to include a cleanup function
Sometimes we might need to take into account that an user might resize the browser window, and that things like sub-menus or advertising banners need to contemplate such resize and behave accordingly; we might be easily tempted to do the following
1import React, { useEffect } from 'react'; 2 3function MyComponent() { 4 useEffect(() => { 5 function handleResize() { 6 console.log(window.innerWidth); 7 } 8 9 window.addEventListener('resize', handleResize); 10 }, []); 11 12 return ( 13 <div> 14 Resize the window and check your console to see the changes. 15 </div> 16 ); 17} 18 19export default MyComponent; 20
... and kinda be okay with it.
(In this code, our component uses useEffect to add an event listener to the window whenever the component mounts. The listener logs the window's inner width whenever the window is resized.)
But there's a problem - if the component unmounts, the event listener will still be there, potentially causing memory leaks. This is where the cleanup function comes in. By returning a function from our useEffect, we're telling React to call this function when the component is about to unmount.
1import React, { useEffect } from 'react'; 2 3function MyComponent() { 4 useEffect(() => { 5 function handleResize() { 6 console.log(window.innerWidth); 7 } 8 9 window.addEventListener('resize', handleResize); 10 11 // Add a cleanup to remove the eventListener 12 // when the component is dismounted 13 return () => { 14 window.removeEventListener('resize', handleResize); 15 } 16 }, []); 17 18 return ( 19 <div> 20 Resize the window and check your console to see the changes. 21 </div> 22 ); 23} 24 25export default MyComponent; 26
Adding a simple return() => { ... }
to your useEffect will make sure that the EventListener that checks the window resize will be removed and that it will not take its toll when there's no more need for it.
2. Failing to use the dependencies' array
In the previous example, as we always wanted at every resize to track the new inner width of the browser window, we didn't need to be concerned with the array of dependencies of useEffect. In a lot of other rather common scenarios, however, we want our useEffect to be triggered when specific values change.
Let's say you have a component that fetches some user data based on a userId prop, and displays whether the user is online or not based on an isOnline state. You would want to refetch this data whenever userId
or isOnline
change. We could do something like this (omitting TypeScript for simplicity):
1import React, { useState, useEffect } from 'react'; 2 3function UserProfile({ userId }) { 4 const [user, setUser] = useState(null); 5 const [isOnline, setIsOnline] = useState(false); 6 7 useEffect(() => { 8 async function fetchData() { 9 const response = await fetch(`/api/user/${userId}`); 10 const data = await response.json(); 11 setUser(data); 12 13 if (data.status === "online") { 14 setIsOnline(true); 15 } else { 16 setIsOnline(false); 17 } 18 } 19 20 fetchData(); 21 // Our dependency array including the values we want to watch over 22 }, [userId, isOnline]); 23 24 return ( 25 <div> 26 <h1>{user ? user.name : 'Loading...'}</h1> 27 <p>{isOnline ? 'Online' : 'Offline'}</p> 28 </div> 29 ); 30} 31 32export default UserProfile; 33
The useEffect hook has [userId, isOnline]
as its dependency array. This means that the fetchData
function will be called not only when the component first mounts, but also whenever userId
or isOnline
change.
If you forget to include a dependency in the array, or include the wrong dependency, it can cause your effect to be run more or less frequently than intended:
- if you leave the array of dependencies empty, the effect will only run once (...which for some cases it might be what you're looking for)
- if you add all the dependencies your editor might suggest you, you might end up re-rendering the component more than you need, making your app slow or with undesired outcomes - don't let the editor tell you better on this one xD
3. Not using AbortController
when dealing with fetch requests
Building up from our previous example, if the component UserProfile
would render as a result of a click on a list of users, one potential problem could be that our component could receive old data as a result of a delayed response.
(Yay, a gif!)
We got ourselves in a very unfavorable race condition.
A race condition is a situation in programming where the behavior of a piece of code depends on the relative timing of multiple threads/processes.
Race conditions often appear when working with Promises, asynchronous functions, or any kind of deferred execution, such as setTimeout or setInterval. These issues can be mitigated by properly managing the flow of execution in asynchronous operations (for example, using async/await or chaining Promises with then).
So how do we prevent our UI from showing the response of a delayed and outdated request?! You know it, AbortController
is here to save the day :D
1import React, { useState, useEffect } from 'react'; 2 3function UserProfile({ userId }) { 4 const [user, setUser] = useState(null); 5 const [isOnline, setIsOnline] = useState(false); 6 7 useEffect(() => { 8 // Create an AbortController 9 const abortController = new AbortController(); 10 11 async function fetchData() { 12 try { 13 // We use abortController in our fetch request to keep track 14 // of a potential abort signal 15 const response = await fetch(`/api/user/${userId}`, { signal: abortController.signal }); 16 17 if (!response.ok) { 18 throw new Error(response.status); 19 } 20 21 const data = await response.json(); 22 setUser(data); 23 24 if (data.status === "online") { 25 setIsOnline(true); 26 } else { 27 setIsOnline(false); 28 } 29 } catch (error) { 30 if (error.name === 'AbortError') { 31 console.log('Fetch aborted'); 32 } else { 33 console.error('An unexpected error happened:', error); 34 } 35 } 36 } 37 38 fetchData(); 39 40 return () => { 41 // Now if userId or isOnline change, the useEffect cleanup will 42 // trigger the abortController and stop any ongoing fetch request 43 // connected to it 44 abortController.abort(); 45 }; 46 }, [userId, isOnline]); 47 48 return ( 49 <div> 50 <h1>{user ? user.name : 'Loading...'}</h1> 51 <p>{isOnline ? 'Online' : 'Offline'}</p> 52 </div> 53 ); 54} 55 56export default UserProfile;
In this updated code
- we create a new
AbortController
instance before the fetch request - the
signal
property from theAbortController
is passed to the fetch options - the
abort()
method is called in the cleanup function returned by useEffect, causing the fetching to be canceled
Basically now, if the userId changes before the fetch has completed, the fetch for the previous userId will be cancelled to prevent setting state on an unmounted component or outdated fetch requests. And well, another great way to prevent memory leaks.
I'd definitely recommend have a look at the documentation for AbortController on mdn web docs (Opens in a new tab), as there you get to see all sorts of examples and exhaust any curiosity about it!
One of my favourite youtubers, Web Dev Simplified (Opens in a new tab), has put this issue in better words than either I or ChatGPT will ever manage, so head out to watch his video about Top 6 React Hook Mistakes Beginners Make (Opens in a new tab), you'll have a great explanation I promise!
The content that I'd like to show here comes from an external website, which has the potential to set a cookie on your browser and track you.
A third-party cookie is a browser cookie set by a website different from the one being visited (in this case, not from oh-no.ooo), and it's typically used for tracking and advertising.
In an effort to respect and protect people's privacy, I leave you the choice of accepting to see the embedded content here or check the content from the source. And well... you can also ignore it too, I guess, but dooooooon't, I promise I'll share only the best stuff! ♥
4. Relying on the order of useEffect calls
The order in which useEffect calls are executed is not guaranteed, meaning that we should definitely not rely on the order of useEffect calls to determine the behavior of your components.
Let's say we have a blog website where we fetch user information and the user's blog posts. We might be tempted to separate the data fetching into multiple useEffects for better separation of concerns:
1import React, { useState, useEffect } from 'react'; 2 3function UserProfile({ userId }) { 4 const [user, setUser] = useState(null); 5 const [posts, setPosts] = useState(null); 6 7 // Fetch user data 8 useEffect(() => { 9 fetch(`/api/user/${userId}`) 10 .then(response => response.json()) 11 .then(data => setUser(data)); 12 }, [userId]); 13 14 // Fetch user posts 15 useEffect(() => { 16 fetch(`/api/posts?userId=${userId}`) 17 .then(response => response.json()) 18 .then(data => setPosts(data)); 19 }, [userId]); 20 21 return ( 22 <div> 23 <h1>{user ? user.name : 'Loading user...'}</h1> 24 <h2>{posts ? `Posts: ${posts.length}` : 'Loading posts...'}</h2> 25 </div> 26 ); 27} 28 29export default UserProfile;
While this separation may seem neat, it could lead to unnecessary multiple renders and potential issues with race conditions (one fetch completing before the other).
A better approach might be to combine the two fetch calls into a single useEffect:
1import React, { useState, useEffect } from 'react'; 2 3function UserProfile({ userId }) { 4 const [user, setUser] = useState(null); 5 const [posts, setPosts] = useState(null); 6 7 // Fetch user data and user posts 8 useEffect(() => { 9 fetch(`/api/user/${userId}`) 10 .then(response => response.json()) 11 .then(data => { 12 setUser(data); 13 return fetch(`/api/posts?userId=${userId}`); 14 }) 15 .then(response => response.json()) 16 .then(data => setPosts(data)); 17 }, [userId]); 18 19 return ( 20 <div> 21 <h1>{user ? user.name : 'Loading user...'}</h1> 22 <h2>{posts ? `Posts: ${posts.length}` : 'Loading posts...'}</h2> 23 </div> 24 ); 25} 26 27export default UserProfile;
In this version, the single useEffect first fetches the user data, then it fetches the user's posts, leaving us with more control over the sequence of events, reducing the number of re-renders and the risk of race conditions.
5. Not using useEffect at all
Some junior developers may not be aware of the useEffect hook, or may not understand when it is appropriate to use it.
Performing side effects in the render function
Some of those instances might look as such:
1// Bad practice 2function UserProfile({ userId }) { 3 // Fetch will happen in the render 4 const user = fetch(`/api/users/${userId}`); 5 6 return <div>{user.name}</div>; 7}
The render function in React should be pure, which means it shouldn't cause any side effects like API calls, subscriptions, or timeouts/intervals. This is because React might call the render function more times than you expect, and having side effects in the render function can lead to unexpected behavior.
And to prevent all that bad, we can simply make use of our friends useEffect and useState:
1// Better practice 2function UserProfile({ userId }) { 3 const [user, setUser] = useState(null); 4 5 useEffect(() => { 6 fetch(`/api/users/${userId}`) 7 .then(response => response.json()) 8 .then(data => setUser(data)); 9 }, [userId]); 10 11 return <div>{user ? user.name : 'Loading...'}</div>; 12} 13
Not Handling Dependent State Correctly
When state depends on props or another state, it might need to be updated inside a useEffect to ensure it stays in sync, otherwise we might not see updated values.
1// Bad practice 2function SquareArea({ edge }) { 3 // Initial value of squared depends on props 4 const [squared, setSquared] = useState(edge * edge); 5 6 return <div>{squared}</div>; 7}
While this might initially appear okay, the problem is that the initial state of squared is set based on the value prop, but useState only sets the initial state once when the component is first rendered. So if the value prop changes in the future, squared will not get updated, because useState does not track the updates of value.
To overcome that, we can do:
1// Better practice 2function SquareArea({ edge }) { 3 const [squared, setSquared] = useState(edge * edge); 4 5 // Update the state when edge changes 6 useEffect(() => { 7 setSquared(edge * edge); 8 }, [edge]); 9 10 return <div>{squared}</div>; 11}
Our beloved useEffect will take care of updating squared
every time the edge
prop will change :D
As we can see, useEffect is a powerful tool we can take advantage of to make our apps snappy and sharp, but we need to be wary on how we use it if we don't want to have headaches trying to debug our own mistakes. 🤯
Let me know if I forgot to mention something!