Hey friends! I'm Christian 👋, an aspiring frontend developer from germany. I'm writing (yet another) post about React's useEffect hook primarily for future reference. I frequently use useEffect, but I happen to struggle with advanced use-cases and complexity from time to time. So, I'll brush up my understanding of useEffect and try to fit it into the bigger picture of React's component lifecycle and JavaScript closures. 🚀
I've started reading up on useEffect in the official documentation of useEffect. I highly recommend you check it out for an in-depth guide about useEffect.
Basics about useEffect
Hooks were introduced in React v16.7.0-alpha, and they provide a way to encapsulate component logic into reusable pieces of code. Additionally hooks can seamlessly interact with different parts of component state or be stateful themselves, which is a major advantage.
Mental Model for useEffect
The useEffect hook is one of the most frequently used hooks provided by React. You can think of useEffect as a replacement for componentDidMount, componentDidUpdate and componentDidUnmount just for functional components all in one.
useEffect offers a way to hook into the components lifecycle and perform side-effects. Side-effects are operations that affect things outside of the component function. Side-effects basically make a function impure if the return value relies of on data outside the function's scope.
In class components you'd think about the lifecycle in terms of "mounting", "updating" and "unmounting", which were related to the lifecycle methods I listed above. But with functional components and hooks it is better to think about component lifecycle in terms of just "rendering".
The Effect
The signature of the useEffect hooks is useEffect(effectFn, dependencyArray)
. Let's talk about the effectFn
parameter first and simply call it our "effect" (as in the official useEffect guide).
Run Effect on Every Render
Important to know: The effect runs on every render by default. This behavior can be customized by using the dependendyArray
, i.e. the second parameter of the hook, which is optional. More on the dependency array later!
import { useEffect } from "react";
export default function MyComponent() {
useEffect(() => {
// inside our effect
});
}
A Word about Closures
useEffect or hooks in general get really interesting when they interact with variables outside their own function body, i.e. in the component's scope. Let's consider a common case where the effect uses a state variable of the component.
import { useEffect, useState } from "react";
export default function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// inside our effect
console.log("I run on every render whatsoever", count);
});
return (
<div>
<span>{count}</span>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
+++
</button>
</div>
);
}
What happens when the component renders initially (which is also called "mounting")?
- The function
MyComponent
is called - The
count
state variable is initialized with a value of 0 - The effect function is initialized and closes over the
count
state variable. Likeconsole.log(count)
is resolving toconsole.log(0)
- The DOM is painted according to the JSX returned from the component
- The effect runs and logs 0 to the console.
If count
is set to a new value, the component must re-render and go through steps 1 to 5. On every render a new effect is initialized and called.
But imagine our effect will be much more expensive and should not necessarily run on each render. Since our effect relies only on count
we only want it to run only when count
changes.
Enter the dependency array!
The Dependency Array
With the dependency array you get fine-grained control about when the effect should run. The dependency array is passed as the (optional) second argument to the useEffect hook.
- If you don't pass a dependency array, the effect will run on every render.
- If you pass an empty array, the effect will run on every render.
- If you pass an array with state variables, the effect will run only when at least one of these variables changes.
Run the effect only on first render
useEffect(() => {
// inside our effect
console.log("I run only on first render!");
}, []); // Empty dependency array
Run the effect only when count changes
import { useEffect, useState } from "react";
export default function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// inside our effect
console.log(count);
}, [count]);
// ^^^^^ if count changes between re-renders, run our effect! If not, skip the effect.
return (
<div>
<span>{count}</span>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
+++
</button>
</div>
);
}
The Cleanup
In some cases, you want to run a function when the component unmounts, i.e. a cleanup function. Commonly, if you have attached event listeners to the DOM, you want to remove them when the component unmounts. Or if you have set an interval once after mounting, you'll want to clear the interval after unmounting.
In order to run a function after unmounting, you must return that cleanup function from the effect.
import { useEffect, useState } from "react";
export default function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount((prevCount) => prevCount + 1);
// ^^^^^^^^^ important: pass a callback function to setCount
// this way the interval will always use the latest count state value
}, 1000);
return () => {
// cleanup function
clearInterval(intervalId);
};
}, []);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
+++
</button>
</div>
);
}
Play with my code on StackBlitz
Have a great time coding! ❤️
Top comments (0)