The useEffect()
hook is probably one of the more complex or difficult to understand hooks in React. However, it’s very powerful and is essentially a critical piece of the framework used when fetching data from APIs, using timers and more.
With that in mind, it’s worth getting your head around it.
What are Side Effects?
React’s main purpose is to render the UI and react to a user’s input. It also manages state and props, and re-renders parts of the UI when needed.
Side effects are essentially the non-React things that your components do. This includes things like:
- Storing data in your browser’s
localStorage
(as opposed to React’s state) - Sending HTTP requests, such as when using
fetch()
- Setting and managing timers (i.e. with
setTimeout()
)
You may notice that the example APIs and methods above are all things you might’ve used in a vanilla JS project. These are all things that are not directly related to rendering something on the screen and so are not things we need React to do.
What problems does useEffect()
solve?
As side effects deal with making things happen outside of React, we don’t necessarily want them to run on every component re-render when its state changes.
Take the example of a request to an API based on the value of a search input
field. Let’s also say that the field is part of a larger component with other fields. Now, if a user was to make a change to one of these other fields which updates the component’s state, the component will re-render and the API request will be sent again even though the relevant field’s value didn’t change. This will cause unnecessary API calls (and potentially cost $$$) and may impact performance.
React’s useEffect()
hook allows us to conditionally run our non-React code based on whether or not some relevant dependencies (i.e. state) have changed.
How does useEffect()
work?
The useEffect()
hook must be used within a functional component.
It takes two arguments:
- A function that should be executed after every component render if the specified dependencies changed
- An array of the dependencies for this effect
The function defined within your hook will run when your component is first rendered due to the fact that the provided dependencies did not exist prior (so therefore have changed), and after that it will only run if a provided dependency changes.
If you provide no dependencies and just an empty array, the hook will only run the once on that initial load. This can be good for data fetching where you don’t necessarily want to update that fetched data again.
If you do not provide any dependencies and no empty array, the effect will run on every render and re-render of your component.
What to Add as a Dependency
You will typically provide state and properties (props
) as dependencies to your useEffect()
hook. State-updating function such as setValue
in a const [value, setValue] = useState()
are guaranteed by React to not change, so you can reference them in your function but don’t need to add them as a dependency. You also don’t need to add any non-React API methods like fetch
, for example. Only add those things that could change when your component re-renders.
Cleanup Function
As our side effects are dealing with things happening outside of React, they aren’t subject to React’s typical component lifecycle. For instance, if a component is destroyed, a timer your set in an effect will still run which can cause issues.
To clean up after our effect, we can simply return
a function from it.
This function will execute before the main function runs, except on the very first render of the component (and thus the first run of the effect). The cleanup function will also run when the component unmounts (again, ensuring we’re cleaning up after ourselves when no aspects of the effect are needed anymore).
Summary
To summarise how to setup your useEffect()
hook:
- Without dependencies, your effect runs every time component is rendered/re-rendered.
- With an empty dependency array, your effect runs only the first time the component is rendered.
- With dependencies, your effect runs only on first render and when those dependencies change.
- With dependencies, a cleanup function will run before the main function runs, but not the first time it runs.
- Without dependencies, the cleanup function will only run when the component unmounts.
Example of useEffect()
usage
A simple timer component that takes a timer duration as a prop:
import { useEffect, useState } from 'react';
let myTimer;
const MyComponent = (props) => {
const [timerIsActive, setTimerIsActive] = useState(false);
const { timerDuration } = props; // using destructuring to pull out specific props values
useEffect(() => {
if (!timerIsActive) {
setTimerIsActive(true);
myTimer = setTimeout(() => {
setTimerIsActive(false);
}, timerDuration);
}
}, [timerIsActive, timerDuration]);
};
With a cleanup function to clear the timer:
useEffect(() => {
const identifier = setTimeout(() => {
console.log('checking form validity')
setFormIsValid(
enteredEmail.includes('@') && enteredPassword.trim().length > 6
);
}, 500)
return () => {
console.log('cleanup function')
clearTimeout(identifier);
}
}, [enteredEmail, enteredPassword]);