December 17, 2022
React’s useEffect hook is a powerful tool for managing side effects in functional components. It allows you to perform tasks that would normally be done in a component’s lifecycle methods, such as making network requests or subscribing to events.
However, it is important to understand when not to use useEffect, as it can be overused and is often a footgun in the sense that it can cause problems if used improperly. It is important to carefully consider the consequences of using useEffect and to use it with caution to avoid potential problems.
One common use case for useEffect is updating the component’s state based on prop changes. For example, let’s say we have a component that displays a list of posts, and also takes an optional postName
to filter posts by.
import { useState, useEffect } from "react";
const PostList = ({ posts, postName }) => {
const [displayedPosts, setDisplayedPosts] = useState(posts);
useEffect(() => {
if (postName) {
setDisplayedPosts(posts.filter((post) => post.name.includes(postName)));
} else {
setDisplayedPosts(posts);
}
}, [posts, postName]);
return (
<ul>
{displayedPosts.map((post) => (
<li key={post.id}>{post.name}</li>
))}
</ul>
);
};
This component uses the useState hook to manage the state of the displayed posts, and the useEffect hook to update the displayed posts based on the list of posts and the post name filter. The useEffect
hook runs on every change to the posts and postName, this approach works, but has it’s pit falls.
Every time the items prop changes, the component will re-render with the updated displayedPosts
state. This may not be a problem if the prop changes infrequently, but if it changes frequently (such as in a real-time data application), the component will re-render unnecessarily, which can impact performance.
Additionally, using useEffect to update the component’s state based on prop changes adds an extra layer of indirection to the component. If a developer goes to the definition of the displayedItems
they will need to also then find all usages of the setDisplayedPosts
callback, causing some level of confusion
Another problem with using a useState
in conjunction with a useEffect
is that, especially with bigger components, it’s not clear when or where a useState is being updated, if there are multiple calls to setDisplayedPosts
in a file it may add a large amount of cognitive overhead to understand what is happening.
Instead of using useEffect to update the component’s state based on prop changes, consider using an inline constant or an inline constant with a useMemo hook for slower operations.
For example, instead of using useEffect and useState in the PostList component, we can simply create an inline const, displayedPosts
import { useState, useEffect } from "react";
const PostList = ({ posts, postName }) => {
const displayedPosts = postName
? posts.filter((post) => post.name.includes(postName))
: posts;
return (
<ul>
{displayedPosts.map((post) => (
<li key={post.id}>{post.name}</li>
))}
</ul>
);
};
This approach avoids the unnecessary re-renders caused by useEffect and keeps the component simpler and more maintainable.
However, sometimes opperations may be expensive. Using an inline constant with useMemo
allows us to perform the calculation only when the posts
or postName
props change, avoiding unnecessary computation whilst still keeping the component simple and maintainable.
import { useState, useEffect, useMemo } from "react";
const PostList = ({ posts, postName }) => {
const displayedPosts = useMemo(
() =>
posts.filter((post) => {
// ... some expensive operation
}),
[posts, postName]
);
return (
<ul>
{displayedPosts.map((post) => (
<li key={post.id}>{post.name}</li>
))}
</ul>
);
};
ℹ️ Note useMemo is not suitable for async operations such as data fetching, as it blocks the main thread, for these use cases (side effects) useEffect and a useState is a very valid combination, or I’d also reccomend utilizing @tanstack/react-query for this!
In conclusion, it is important to use caution when using the React useEffect hook and to carefully consider the consequences of its usage. In some cases, it may be more efficient and maintainable to use inline constants or useMemo to update the component’s state based on prop changes, rather than relying on useEffect. It is essential to be aware of the potential for unnecessary re-renders and mutability when using useEffect, and to choose the most appropriate approach for the specific use case.