Hooks are an essential part of React, and useEffect is one of the most common ones.
In this article, I want to explain how useEffect works and when it should be used. This is mostly based on how I understand it and how I’ve used it in my own projects.
useEffect helps us deal with side effects in React. Side effects are things we want to do outside of rendering UI. For example, manipulating the DOM, making API calls, setting up event listeners, or working with timers.
The main idea is simple. useEffect allows us to run some code after a component renders.
Running an effect after every render
If we do not pass a dependency array, the effect will run after every render.
useEffect(() => {
console.log("Component rendered");
});
Every state change that causes a re-render will trigger this effect. This is usually not what we want, but it can be useful in some cases.
Running an effect only once
If we pass an empty dependency array, the effect will run only once, when the component first renders.
useEffect(() => {
console.log("Component mounted");
}, []);
This is often used for things like fetching data when the page loads or setting up initial logic.
Running an effect when values change
If we pass values inside the dependency array, the effect will run only when one of those values changes.
useEffect(() => {
console.log("List changed");
}, [list]);
In this example, the effect runs only when list changes, such as when we add or remove an item. This makes useEffect more predictable and easier to reason about.
Infinite loops (a common mistake)
One important thing to watch out for is infinite loops. If an effect updates state, and that same state is also listed as a dependency, the effect can keep running forever.
useEffect(() => {
setCount(count + 1);
}, [count]);
This will cause the effect to run again and again. Spoiler alert: I’ve done this before, and it’s not fun to debug.
Cleanup in useEffect
When we work with events, subscriptions, or timers, we should clean them up. React gives us a way to do this by returning a function from useEffect.
useEffect(() => {
const handleResize = () => {
console.log("Window resized");
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
Without cleanup, we can end up with memory leaks or performance issues, especially in larger applications.
Do we always need useEffect?
So far, this covers the basics of useEffect. From here, I want to make one important point. Just because useEffect exists does not mean we should always use it.
In many cases, adding effects can make code harder to read and can introduce unexpected bugs. This idea comes from a talk by Dan Abramov, where he explains that many effects are unnecessary and that some logic can live directly in render or be handled differently.
I haven’t deeply researched this yet, but from my own experience, this idea makes sense. Overusing useEffect can complicate things very quickly.
This is something I plan to explore more as I continue learning and building with React.