React Hooks aim to solve the difficulties of logic reuse by enabling us to write components that have access to features like state, context, lifecycle methods, ref, etc. In this article, we’ll focus on the powerful useEffect hook, which allows us to perform side effects in our function components.
The concept of Hooks in React was introduced in early 2019 and for the last few years has been a commonly used utility in the world of React development.
For those who may be new to React, React Hooks aim to solve the difficulties of logic reuse by enabling us to write functional components that have access to features like state, context, lifecycle methods, ref, etc. without having to write a class component.
Many different hooks are immediately available to us when working with React, such as:
useState
HookuseEffect
HookuseContext
HookuseRef
HookReact also gives us the capability to create our unique hooks to reuse custom stateful logic between components.
In the future, we’ll spend more time discussing all the different capabilities React Hooks give us, but for this article we’ll direct our attention to the powerful useEffect
hook.
The useEffect
hook allows us to perform side effects in our function components. Side effects are essentially anything where we want an “imperative” action to happen. API calls, updating the DOM, subscribing to event listeners—these
are all side effects that we might like a component to undergo at different times.
The useEffect
hook doesn’t return any values but instead takes two arguments. The first being required and the second optional.
When written in code, the useEffect
hook looks like the following:
import React, { useEffect } from "react";
export const FunctionComponent = () => {
useEffect(() => {
// effect callback function
}, [] /* optional dependencies array */);
return (
// ...
);
}
There are three stages of a lifecycle of a React component where we may want to run a side effect:
Assume we wanted to place a console.log()
message within an effect callback in a function component.
import React, { useEffect } from "react";
export const FunctionComponent = () => {
useEffect(() => {
console.log("run for every component render");
});
return (
// ...
);
}
By default, the effect stated in a useEffect
hook runs when the component first renders and after every update. If we run the above code, we’ll notice the console.log()
message is generated as our component is rendered.
If our component was to ever re-render (for example due to a component state change), we’ll see the side effect run on every re-render that occurs.
The second argument of the useEffect
hook is optional and is a dependency list that allows us to tell React to skip applying the effect only until in certain conditions. In other words, the second argument of the useEffect
hook
allows us to limit when the effect is to be run. If we simply place a blank empty array as the second argument, this is how we tell React to only run the effect on initial render.
import React, { useEffect } from "react";
export const FunctionComponent = () => {
useEffect(() => {
console.log("run only for first component render (i.e. component mount)");
}, []);
return (
// ...
);
}
With the above code, the console.log()
message will only be fired when the component first mounts and won’t be generated again even if the component was to re-render multiple times.
Instead of having an effect run once in the beginning and on every update, we can attempt to restrict the effect to run only in the beginning and when a certain dependency changes.
Assume we wanted to fire off a console.log()
message every time the value of a state property was to change. We can achieve this by placing the state property as a dependency of the effect callback.
import React, { useState, useEffect } from "react";
export const FunctionComponent = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(
"run for first component render and re-run when 'count' changes"
);
}, [count]);
return (
<button onClick={() => setCount(count + 1)}>
Click to increment count and trigger effect
</button>
);
};
Above, we have a button in the component template responsible for changing the value of the count
state property when clicked. Whenever the count
state property is changed (i.e., whenever the button is clicked), we’ll notice
the effect callback is run and the console.log()
message is fired!
If we had a different state property change, we’ll notice the effect callback won’t be rerun and the console.log()
message won’t fire since this property isn’t a dependency of the effect.
An effect callback gets run every time on the initial render of a component and when we’ve specified when an effect should run (i.e., when an effect dependency has changed). The useEffect
hook also provides the ability to run a cleanup
after the effect. This can be done by specifying a return function at the end of our effect.
import React, { useState, useEffect } from "react";
export const FunctionComponent = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("run for every component render");
return () => {
console.log("run before the next effect and when component unmounts");
};
}, [count]);
return (
<button onClick={() => setCount(count + 1)}>
Click to increment count and trigger effect
</button>
);
};
In the example above, we’ll notice the cleanup function message be fired before the intended effect is ever run. In addition, if our component ever unmounts—the cleanup function will run as well.
A good example of when we might need a cleanup is when we set up a subscription in our effect but want to remove the subscription whenever the next subscription call is to be made, to avoid memory leaks.
This article covers all the different ways the useEffect
hook can be utilized to run side effects in components. At any time we want an “imperative” action to occur in our component (e.g., call an API, subscribe to an event listener,
etc.), we should automatically think of leveraging the useEffect
hook to run a side effect for a particular moment in the lifecycle of the React component.
Hassan is a senior frontend engineer and has helped build large production applications at-scale at organizations like Doordash, Instacart and Shopify. Hassan is also a published author and course instructor where he’s helped thousands of students learn in-depth frontend engineering skills like React, Vue, TypeScript, and GraphQL.