Typically, you work with props and local state in React components. However, sometimes you need to interact with the "outside" world, such as API calls, timers and intervals that are part of Web API, event listeners, non-React libraries, and so forth. All these operations are typically called "side effects". To handle them inside functional components, you need to use the useEffect hook. It replaces componentDidMount(), componentDidUpdate(), componentWillUnmount() lifecycle methods commonly used in class-based components.
In this topic, you'll understand what useEffect is and when to apply it in React applications.
What is useEffect?
Before defining the useEffect hook, let's clarify what the side effects are. Imagine a programmer building web applications in the office. Their primary job is coding, but they also join daily meetings, solve design problems with a designer, answer phone calls, or answer questions in chats. These little "operations" are side effects because they aren't about writing code.
The same happens in the React world. The main goal of React is to render UI to the screen, but it also needs to handle tasks from the outside without losing its primary rendering focus. The useEffect mechanism helps you manage various tasks that should be done alongside the main rendering process but aren't directly involved in displaying the UI.
You typically use this hook in three main cases:
Network Requests: Fetching data from an API, sending data to a server, or subscribing to a real-time service like a WebSocket.
Timers and Intervals: JavaScript timers (like
setTimeoutorsetInterval) are considered outside of the component.The DOM: While React components produce a DOM representation, direct manipulation of the DOM is considered "outside" of the component because React prefers a declarative approach to rendering.
In the next sections, we'll see actual examples for each of these cases.
Fetching data
Working with APIs is common in frontend development; most of the time, you will use the useEffect hook. Let's create a website for a cat shelter. So, those who want to take a cat home can browse the site, look at the pets, and possibly bring a new friend home. We have a backend API that stores pictures of our cats. Let's make a request and display these images on the screen.
import React, { useEffect, useState } from 'react';
function Cats() {
const [cats, setCats] = useState([]);
const fetchCats = async () => {
const response = await fetch('https://api.thecatapi.com/v1/images/search?limit=10');
const data = await response.json();
setCats(data);
};
useEffect(() => {
fetchCats();
}, []);
return (
<div>
{cats &&
cats.map((cat) => (
<img src={cat.url} key={cat.id} width="300px" height="300px" />
))}
</div>
);
}
export default Cats;Firstly, we initialized the cats constant and assigned it with an empty array. The fetchCats function then calls an API and saves the data into the cats array.
Our main focus here is useEffect; let's break it down. It takes two arguments: the first one is a callback where we placed our fetching function, and the second one is an optional dependency array. It's called "dependency" because fetchCats depends on the variables included in the array. Currently, the array is empty, meaning that useEffect will run only once after the initial render. This makes sense because we only need to call an API once and then render images to the screen.
It's best to initialize fetching functions above useEffect and then call them inside the hook. Alternatively, you can declare the function inside the hook and call it right away, rather than immediately calling the function without saving it to a variable.
Suppose a user looked through the 10 pictures that displayed but couldn't find their favorite breed. In this case, we want to give them a chance to call an API again and send back another set of random cat's pictures. To do this, we need to create another state variable, preferably of a boolean type, and a button that toggles the state between true and false. Here's what it looks like:
Adding new state and
useEffect:
const [isNew, setIsNew] = useState(false);
const getNewCats = () => {
setIsNew(!isNew)
};
useEffect(() => {
fetchCats();
}, [isNew]);Adding a button:
<button type="button" onClick={getNewCats}>New cats</button>The click event of the button triggers the getNewCats function that flips the isNew variable from true to false. Whenever this variable changes, we intend to make a new API call, and that's why we included this state in the dependency array. More substantial applications may have more dependencies; it's not limited to one.
If you don't provide a dependency array to the useEffect hook, it means that the effect will run after every render of your component. This includes the first mount of the component and every update it receives. This behavior can be handy when you want to execute some code every time the component updates, no matter the reason for the update. However, it can also lead to performance issues, so to be safe, it's better to provide a dependency array unless the risks are justified.
The output of the Cats' app:
You might wonder why not simply call the API anywhere inside the component without using the hook? Great question! That's because the component may re-render multiple times, and each re-render will cause the functions that sit inside the component to run again. In our case, each API call updates the cats state, each update re-renders the component, then the API is called again and so on. This will trap us in an infinite loop.
Out of curiosity, comment out the useEffect and call the fetchCats() function right below where it's declared. You'll see the page freeze. The useEffect hook gives us better control over async operations and their dependencies.
Timers and intervals
The setTimeout and setInterval functions are part of BOM, so React also considers them the "outside world". Usually, you'll find them inside useEffect.
setTimeout
You can use setTimeout in various situations when you want to introduce delays before executing specific actions. For instance, in a chat application, you may want to display a typing indicator for a few seconds before showing the received message. It's also usable for creating animations or transitions by applying incremental changes to an element's style over time. Another example that we encounter all the time is some type of advertisement banner that pops up after browsing the site for a specified time.
You can use setTimeout within useEffect to create a delayed message that encourages users to subscribe to the newsletter:
import React, { useEffect, useState } from 'react';
function Message() {
const [message, setMessage] = useState('');
useEffect(() => {
const timerId = setTimeout(() => {
setMessage('Stay tuned and subscribe to the newsletter!');
}, 3000);
return () => clearTimeout(timerId);
}, []);
return <div>{message ? message : 'Message will appear in 3 seconds...'}</div>;
}
export default Message;Initially, message is an empty string. The useEffect creates a delay of 3 seconds before updating the message state. The UI: when the message equals some value, it displays; otherwise, it states, "Message will appear in 3 seconds". However, there is some return statement inside useEffect. Let's figure out what it is!
When you set a timeout or interval, it schedules a task for execution after a certain delay or at regular intervals. However, if there isn't proper cleanup of these tasks, they can keep running even after the component that set them has unmounted or updated. This can lead to memory leaks and unexpected behavior in your application. The return statement in useEffect defines a cleanup function that executes when the component unmounts or before the effect runs again on a subsequent render.
The proper cleanup after setTimeout and setInterval involves canceling or clearing the scheduled tasks before the component unmounts or updates. This ensures that any pending tasks are properly stopped, preventing them from running unnecessarily and causing issues.
setInterval
The setInterval is useful when you need to run a certain function periodically. For instance, a stock market tracker that fetches and updates stock prices every few seconds. Or in slideshows and carousels to cycle through a series of images or content, allowing automatic switch to the next item after a specified interval. It's also used to periodically check for updates from a server. For instance, in a real-time chat application, you might use it to fetch new messages from the server at regular intervals.
Imagine you need to create a quiz for students, and want to give them 5 seconds to submit their answer. We can do this inside useEffect. Let's create a component that will have a timer. When the timer is greater than 0, the submit button should be active; however, when time is up, let's disable the button.
If you want to challenge yourself, try doing it on your own, then we'll walk through the solution together.
Basically, your first step will be creating a timer state variable initially set to 5. The useEffect will look like the previous example, except for now it should decrease the timer by one every second, and include the timer state in the dependency array because we want to run useEffect every time the timer changes. Here's the code:
import React, { useEffect, useState } from 'react';
function Message() {
const [timer, setTimer] = useState(5);
useEffect(() => {
const timerId = setInterval(() => {
setTimer(timer - 1);
}, 1000);
return () => clearInterval(timerId);
}, [timer]);
return <div><h1>Time left to submit the answer: {timer}</h1>
<button disabled={timer === 0 ? true : false}>Submit answer</button>
</div>;
}
export default Message;If you open it in the browser, you should see that the timer works, but it keeps counting into negative numbers. Let's stop it when it reaches zero; also, we need to finish the disabled functionality of the button so that it's inactive when the timer equals zero.
Before const timerId inside useEffect write this code to stop the timer:
if (timer === 0) {
return;
}Also, change the button to:
<button disabled={timer === 0 ? true : false}>Submit answer</button>Voilà, you've created a properly functioning timer! Here's the result:
DOM manipulations
React provides a straightforward method to manage DOM changes through its state and props system. Despite this, there are valid situations where you may need to manipulate the DOM directly, for instance, when working with non-React libraries, or managing particular animations or focus tasks.
Also, if you want to listen to events on the window, document, or other global objects, you should use useEffect as these objects aren't part of your component's rendered output.
A simple example of this is changing the page's title. Although it's not a frequent task, it's straightforward and easy to grasp. See how you can change the title to a new string like this inside the useEffect:
useEffect(() => {
document.title = 'Love and Peace!';
}, []);You can have multiple useEffect calls on a single page. It's beneficial to create a separate useEffect for each operation. For instance, one for altering the title, another for fetching data.
Forms are an essential part of frontend development; they are omnipresent, and your job is to make them user-friendly. One way to enhance user experience is by implementing autofocusing. This feature automatically focuses the input field as soon as the user loads the page. Let's do it!
import React, { useEffect } from 'react';
function AutoFocusInput() {
useEffect(() => {
// Directly manipulate the DOM to focus the input element
const input = document.querySelector('.input')
input.focus()
}, []);
return <input type="text" className="input" />;
}
export default AutoFocusInput;In this example, you directly query the input field using pure JS. Then, you use the focus method on the selected input element to focus it. This ensures the input field is in focus as soon as the component mounts and the effect runs, enabling the user to start typing right away.
A scrolling event inside useEffect is pretty common. Let's assume you're building an Instagram clone. With billions of photos stored in the database, it's inefficient to send them all at once. Instead, you can display ten pictures and, as the user scrolls down to the last photo, send another request for an additional set of images.
Another instance is if you trigger a modal and wish to disable scrolling for the user. In such a situation, you can create a no-scroll CSS class with overflow set to hidden. When the modal is open, add this class to the body; remove it when the modal is closed. Here's a basic example:
import React, { useEffect } from 'react';
function NoScrollBody() {
useEffect(() => {
document.body.classList.add('no-scroll');
return () => {
document.body.classList.remove('no-scroll');
};
}, []);
return <div>Modal is open! No scrolling!</div>;
}
export default NoScrollBodyAs shown, the useEffect hook is valuable when you want to manually update the DOM or add event listeners at appropriate times.
Conclusion
To sum up, the useEffect hook in React acts as a flexible tool for managing side effects—essential operations that happen outside the primary process of rendering UI components. As you might have noticed, useEffect gives you the capability to smoothly integrate API calls, control timers and intervals, and execute direct DOM manipulations in your functional components. By supplanting the lifecycle methods of class-based components, useEffect offers a more organized, declarative way to handle side effects, in line with React's overall design approach.