How to use the React useEffect hook
The React useEffect
hook is used to perform side-effects in function components.
Here are some examples of common side effects:
- data fetching
- subscriptions
- manually changing the DOM
Using the useEffect hook
The useEffect
hook accepts 2 arguments:
useEffect(callback, [dependencies]);
-
Callback: The callback is the function that performs the logic for a side-effect. The callback is executed right after changes were being pushed to DOM.
-
Dependencies: The dependencies array is an optional array of dependencies. When dependencies are specified,
useEffect
will executes its callback function only if the dependencies have changed between renderings. The dependencies argument is used to control when the side-effect should run.
useEffect and the component lifecycle
We can configure the useEffect
hook to perform side effects in a component at different times. These include:
- After every render/re-render
- After the first render
- After certain props or state variables change
// This gets called after every render
// (the first render, and every render after that)
useEffect(() => {
console.log('[useEffect] on every render');
});
// This gets called on the first render
useEffect(() => {
console.log('[useEffect] on first render');
}, []);
// This gets called on the first render
// and when the prop or state variable 'name' changes
useEffect(() => {
console.log('[useEffect] on first render and name change');
}, [name]);
It's important that we configure useEffect
to run at the right time and that we try to avoid unnecessarily re-running effects because they can affect performance.
Organizing side-effects
It's a good practice to avoid performing side-effects directly in the body of the component, which is where we return the HTML output that is displayed on the screen.
const Greeting = ({ name }) => {
const message = `Hi ${name}`;
// Bad practice
document.title = `Greetings ${name}`;
return <div>{message}</div>;
}
The updating of the document title is a side-effect here because it's not part of the component output in the return statement.
We should also avoid updating the document title every time the Greeting
component renders. We should only set the document title when the name
prop of the component changes.
import { useEffect } from 'react';
const Greeting = ({ name }) => {
const message = `Hi ${name}`;
useEffect(() => {
document.title = `Greetings ${name}`;
}, [name]);
return <div>{message}</div>;
};
We fixed the Greeting
component by moving the updating of the document title in the callback of useEffect
. We added name
as a dependency to useEffect
so that it doesn't run unnecessarily on every component render, but only when the name
actually changes between renders.
Asynchronous functions
We can use the useEffect
to fetch component data from the client-side of our application. The useEffect
hook itself cannot be an asynchronous function. However, it can invoke asynchronous functions.
useEffect(() => {
if (!url) {
return;
}
const fetchData = async () => {
const response = await fetch(url);
const data = await response.json();
console.log(data);
};
fetchData();
}, [url]);
Let's assume that we have a URL from where to fetch data from. We can add a url
variable to the useEffect
hook as a dependency. Within the useEffect
callback function, if the url
is not defined, we exit the function. However, if the url
is defined, then we call the fetchData()
function asynchronously.
Notice that we do not need any await
keyword before the call to fetchData()
. The useEffect
hook automatically handles this for us.
Within the fetchData()
function, we use the Fetch API to fetch data from the url
and then output it to the console.
Side-effect cleanup
Some side-effects performed by the useEffect
hook need to be cleaned up. Here are some examples:
- Closing a web socket
- Clearing timers
We can create a side-effect cleanup by making the callback function of useEffect
return a function.
useEffect(() => {
// side-effect
return function cleanup() {
// side-effect cleanup function
};
}, [dependencies]);
After the component's first render, useEffect
will invoke its callback that performs a side-effect. The cleanup function is not invoked at this point.
On subsequent component renderings, useEffect
will invoke the cleanup function from the previous side-effect that was executed. It does this to clean up after the previous side-effect. Once the execution of the cleanup function is complete, useEffect
will then run the current side-effect for the current rendering of the component.
When a component is unmounted, that is, removed from view on the screen, useEffect
will also invoke its cleanup function to clean up after the latest side-effect.
Side-effect cleanup example
Let's take a look at an example of cleaning up a side-effect.
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const key = setInterval(() => {
console.log(count);
// triggers a component re-render
setCount((prevCount) => prevCount + 1);
}, 1000);
// cleanup function
return () => clearInterval(key);
}, [count]);
return <span>{count}</span>;
};
This component increments a counter by one every second. The cleanup function is used to cancel the previous timer before starting a new one. This is to avoid spawning a new timer every second and having many of them running at the same time.
After 1 second, setCount
updates count
to 1. The state update triggered by setCount
causes the component to re-render. When the component re-renders, it calls the useEffect
cleanup function to stop the previous timer. Once the cleanup function is complete, useEffect
proceeds by starting a new timer that waits 1 second before updating count
to 2 with setCount
.
What happens if we don't include a cleanup function as the last line of the useEffect
function? The answer is that the clearInterval
function will never be called and a new interval will start every second. This would cause multiple intervals to run simultaneously, making the count value jump uncontrollably in large increments all the way to infinity.
The dependency pitfall
Here is an example of a common pitfall with the useEffect
hook related to missing dependencies.
const ChangeCounter = () => {
const [value, setValue] = useState("");
const [count, setCount] = useState(-1);
// Danger!
useEffect(() => setCount(prevCount => prevCount + 1));
const onChange = ({ target }) => {
setValue(target.value);
};
return (
<div>
<input type="text" value={value} onChange={onChange} />
<div>{count} changes.</div>
</div>
);
};
This component counts the number of changes performed on a text box by the user.
When this component loads, there is an immediate infinite loop problem. Why is that? Let's take a closer look at what's going on.
The dependency pitfall problem
Since useEffect
has no dependency provided, it will run on the first component render and every re-render after that. The callback of useEffect
makes use of setCount
which updates the state of count
and causes a component re-render. The useEffect
callback will run on every re-render and trigger a new re-render, creating an infinite loop where count
increments quickly to infinity, without any changes to the text box.
The dependency pitfall solution
We can fix this by providing the right dependencies for useEffect
.
useEffect(() => {
setCount(prevCount => prevCount + 1);
}, [value, setCount]);
We provide value
as a dependency, since we only want the counter to run when the value of the text box changes. We also provide setCount
as a dependency, since the callback of useEffect
makes use of this method to update the state of count
.
In summary, the infinite loop of component renderings is caused by updating local state in useEffect without having any dependency argument. To avoid the infinite loop, we must properly manage useEffect’s dependencies. This allows us to control when the side-effect should run.
The object pitfall
Another common pitfall with the useEffect
hook is the object pitfall. It occurs when we use objects as dependencies.
const ChangeCounter = () => {
const [item, setItem] = useState({ value: "", count: -1 });
useEffect(() => {
// a new object for 'item' is created here
setItem(prevItem => ({ ...prevItem, count: prevItem.count + 1 }));
}, [item]);
const onChange = ({ target }) => {
setItem({ ...item, value: target.value });
};
return (
<div>
<input type="text" value={item.value} onChange={onChange} />
<p>{item.count} changes.</p>
</div>
);
}
This component counts the number of changes performed on a text box by the user. Rather than just using local state to store the number of changes like the previous example, we now use an object to store the last value entered and the number of changes.
When this component loads, there is an immediate infinite loop problem. Why is that? Let's take a closer look at what's going on.
The object pitfall problem
Remember that a useEffect
hook with a dependency argument of []
or [dependency]
, where dependency is a variable, will get called on the first component render.
In the example above, the item
object is used as a dependency of useEffect
. Inside the useEffect
callback, setItem
gets called. It increments the counter by one but in doing so, it also creates a new object for item
.
Since item
is now a new object, useEffect
detects that its item
dependency has changed and re-runs its callback function. This causes another setItem
invocation that again updates the state with a new item
object. Thus, we have an infinite loop problem.
The object pitfall solution
We can solve the infinite loop problem by changing the dependency from item
to item.value
. We only want the side-effect to run and increment the count
for us when the item's value
is updated by a change made to the text box.
useEffect(() => {
setItem(prevItem => ({ ...prevItem, count: prevItem.count + 1 }));
}, [item.value]);
In summary, avoid using objects as useEffect
dependencies. Use only a specific property of an object instead.
Conclusion
The useEffect
hook manages side-effects in React function components. The callback argument holds the side-effect logic. The dependencies argument allows you to add props or state variables to useEffect
as dependencies.
The useEffect
hook will invoke its callback function after the component has first mounted (first initial render), and then on subsequent component re-renders, if any of its dependencies has changed.
The useEffect
hook can be an effective way to run asynchronous operations within a component.
Remember to avoid the dependency and object pitfalls to make the most effective use of the useEffect
hook.