Easily restore your project to a previous version with our new Instant One-click Backup Recovery

React useEffect() - A complete guide

Master React useEffect Hook with our complete guide, comparing it with useState and exploring its role with React Server Components. Practical examples included.
Joel Olawanle

Joel Olawanle

Aug 26, 2024
React useEffect() - A complete guide

Understanding how to use the useEffect Hook is crucial for any developer working with React applications. It allows you to perform a side effect in your function component, similar to lifecycle methods in class components like componentDidMount, componentDidUpdate, and componentWillUnmount.

Whether you are a beginner or a pro, handling side effects can quickly become overwhelming without a solid grasp of useEffect.

This guide explains the useEffect Hook, compares it with useState, explores its relationship with React Server Components, and provides practical examples to help you master it.

#Why does useEffect exist?

In the early days of React, we relied on class components to manage side effects and other complex logic, while functional components were primarily used to render UI.

Managing side effects in class components required using lifecycle methods, which often led to complex and fragmented code. For example, we needed component lifecycle events to handle different stages of the component's lifecycle:

import { Component } from 'react';
class DataFetchingComponent extends Component {
constructor(props) {
super(props);
this.state = {
data: null,
};
}
componentDidMount() {
// Fetch data when the component mounts
this.fetchData();
}
componentDidUpdate(prevProps) {
// Refetch data when props change
if (this.props.someValue !== prevProps.someValue) {
this.fetchData();
}
}
componentWillUnmount() {
// Cleanup before unmounting
this.cleanup();
}
fetchData() {
// Fetch data logic
}
cleanup() {
// Cleanup logic
}
render() {
return <div>Data: {this.state.data}</div>;
}
}

This approach can quickly become cumbersome because side-effect logic is scattered across multiple lifecycle methods, making the code harder to follow and maintain.

React Hooks were introduced in version 16.8 to address these issues, allowing developers to "hook into" various React features within functional components. The built-in Hooks, like useEffect, consolidate side effect logic into a single place, simplifying the code and making it easier to manage.

Editor's Note

f you're new to React, focus on learning functional components and Hooks like useEffect. Class-based components and lifecycle methods are rarely used in modern React development.

#How does the useEffect Hook work?

As mentioned, the useEffect Hook allows you to perform common side effects in function components. These side effects are tasks outside the component scope, such as fetching data, interacting with the DOM, or managing subscriptions.

The useEffect Hook takes two arguments:

  1. A callback function that contains the side effect logic.
  2. An optional array of dependencies that controls when the effect runs.

Here is the basic structure of a useEffect block:

useEffect(() => {
// Side effect logic
}, [dependencies]); // Dependencies array (optional)

Breaking down the above structure, the callback function inside useEffect runs after the component renders. It contains the side effect logic. The second argument, the array [dependencies], tells React to re-run the effect whenever any value in the array changes. If you have an empty dependency array, the effect runs only once after the initial rendering.

undefined

You can also use an optional effect cleanup function, which runs before the component unmounts or before the effect re-runs. This is useful for tasks like unsubscribing from events or canceling network requests.

useEffect(() => {
// Side effect logic
return () => {
// Cleanup logic (optional)
};
}, [dependencies]); // Dependencies array (optional)

Practical example: Fetching data with useEffect Hook

Let's look at a practical example of using useEffect to fetch blog posts from Hygraph a headless CMS that leverages GraphQL to serve content to your applications into a React application.

Editor's Note

You can clone a project from Hygraph or choose any project from the marketplace that suits your needs. Then go to the Project settings page > Endpoints, and copy the High-Performance Content API endpoint to which you can request API.

In your React project, import the Hooks you’ll use (i.e. useState and useEffect), along with the graphql-request library, which will help us make GraphQL requests:

import { useState, useEffect } from 'react';
import { request } from 'graphql-request';

Next, define the GraphQL query to fetch blog posts from Hygraph. This query specifies the fields you want to retrieve:

const query = `
{
posts {
id
slug
title
excerpt
coverImage {
url
}
publishedAt
author {
name
picture {
url
}
}
}
}
`;

You will then use the useState Hook to create a state variable for storing the fetched data. This state will hold your data once they are retrieved from the API:

const [blogPosts, setBlogPosts] = useState([]);

Now, use the useEffect Hook to fetch the data when the component mounts. The useEffect function runs the fetch logic after the initial render.

useEffect(() => {
const fetchBlogPosts = async () => {
const { posts } = await request(
import.meta.env.VITE_HYGRAPH_API,
query
);
setBlogPosts(posts);
};
fetchBlogPosts();
}, []);

Inside the useEffect Hook, we define an async function fetchBlogPosts that fetches data from the Hygraph API and updates the state with the fetched data. The empty dependency array [] ensures this effect runs only once after the initial render.

Finally, you can render the fetched data as you wish.

return (
<>
{blogPosts.length > 0 ? (
<div>
{blogPosts.map((post) => (
// output data
))}
</div>
) : (
<div>Loading...</div>
)}
</>
);

With this setup, you have a basic implementation of fetching and displaying data from Hygraph using the useEffect Hook in a React application.

Editor's Note

With the introduction of server components in React, Hooks like useEffect can only be used in client components. Make sure to define your component as a client component when using Hooks. As of today, this feature is only implemented in Next.js v13 and above.

Next, let’s explore the dependency array in detail and explain how it affects the behavior of the useEffect Hook.

#Understanding the dependency array in useEffect

The array of dependencies in useEffect controls when the effect runs. Using it correctly can help avoid unnecessary renders and potential bugs. For example, without an array of dependencies, useEffect runs after every render.

useEffect(() => {
console.log('Effect runs after every render');
});

This can lead to performance issues because the effect runs too frequently. To control this behavior, you can use the dependency array. For example, to run the effect once after the initial render, provide an empty dependencies array:

useEffect(() => {
console.log('Effect runs only once after initial render');
}, []);

This is useful for initializing logic that should only run once. You can also specify dependencies to control when the effect runs. The effect will be re-run only when one of the dependencies changes.

const [count, setCount] = useState(0);
useEffect(() => {
console.log(`Effect runs when "count" changes: ${count}`);
}, [count]); // Effect runs when 'count' changes

In the code above, the effect runs only when the count state changes. This prevents unnecessary re-renders, leading to performance optimization.

Common pitfalls and errors

Misusing the dependency array can lead to unexpected behavior and bugs. Here are some common issues and how to avoid them:

1) Missing dependencies: If you omit a dependency, the effect won't run when that dependency changes, leading to stale state or props.

const [count, setCount] = useState(0);
const [text, setText] = useState('');
useEffect(() => {
console.log(`Effect runs when "count" changes: ${count}`);
console.log(`Text is: ${text}`);
}, [count]); // 'text' is missing from the dependency array

In the code above, if text changes, the effect won't run, which might cause issues.

2) Unnecessary dependencies: Including unnecessary dependencies can cause the effect to run too often, impacting performance. For example, in the code below, if only the count variable triggers the effect, then text is an unnecessary dependency:

const [count, setCount] = useState(0);
const [text, setText] = useState('');
useEffect(() => {
console.log('Effect runs on every render');
}, [count, text]); // Including both 'count' and 'text' as dependencies

3) Stable references: If you include a non-stable reference, such as an object or function, the effect may run more frequently than intended.

const [count, setCount] = useState(0);
const fetchData = () => {
// fetch data
};
useEffect(() => {
fetchData();
}, [fetchData]); // 'fetchData' is a non-stable reference

To avoid this, use the useCallback Hook to memoize the function:

const fetchData = useCallback(() => {
// fetch data
}, []); // Dependency array to ensure 'fetchData' is stable
useEffect(() => {
fetchData();
}, [fetchData]);

4) Infinite loops: This can occur if the effect updates state in a way that causes the effect to re-run continuously. Avoid updating state within an effect unless you have a controlled mechanism to prevent an infinite loop as shown in the code below.

import { useState, useEffect } from 'react';
function InfiniteLoopExample() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`Effect runs with count: ${count}`);
setCount(prevCount => prevCount + 1); // This will cause an infinite loop
}, [count]); // Dependency array includes 'count'
return <p>Count: {count}</p>;
}
export default InfiniteLoopExample;

In this example, the useEffect updates count, which causes the React component to re-render and the useEffect to run again, creating an infinite loop. To avoid this, you can add a condition or use a timeout to control the state update:

import { useState, useEffect } from 'react';
function ControlledExample() {
const [count, setCount] = useState(0);
useEffect(() => {
if (count < 10) { // Add a condition to control updates
const timer = setTimeout(() => {
setCount(prevCount => prevCount + 1); // Increment count after a delay
}, 1000);
return () => clearTimeout(timer); // Cleanup timer
}
}, [count]); // Dependency array includes 'count'
return <p>Count: {count}</p>;
}
export default ControlledExample;

Here, the state update is controlled to prevent an infinite loop by adding a condition that stops updates after count reaches 10. This ensures the effect doesn't run continuously without a break.

Editor's Note

React’s ESLint plugin can help identify these concerns around missing dependencies and throw a warning.

#Utilizing cleanup functions

The useEffect Hook not only allows you to perform side effects but also provides a way to clean up those effects when the component unmounts or before re-running the effect. This is done using cleanup functions.

An effect cleanup function is returned from the useEffect callback. React calls this function to clean up any side effects that need to be undone. This is useful for tasks like canceling network requests, removing event listeners, or clearing timers.

Let's look at an example where we add an event listener and then clean it up when the component unmounts:

import { useEffect } from 'react';
function WindowResizeComponent() {
useEffect(() => {
const handleResize = () => {
console.log('Window resized:', window.innerWidth);
};
window.addEventListener('resize', handleResize);
// Cleanup function
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div>
<p>Resize the window to see the effect in action.</p>
</div>
);
}
export default WindowResizeComponent;

In this example, we add a resize event listener to the window object, and then the effect cleanup function removes it when the React component unmounts.

Similarly, timers like setTimeout or setInterval can also be cleaned up using the effect cleanup function:

import { useState, useEffect } from 'react';
function TimerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// Cleanup function
return () => {
clearInterval(timer);
};
}, []);
return (
<div>
<p>Count: {count}</p>
</div>
);
}
export default TimerComponent;

In this example, we use setInterval to update the count every second, and then the cleanup function clears the interval when the component unmounts, preventing memory leaks.

You may ask, why should we clean these timers and listeners? This is because leaving these running can cause memory leaks and unexpected behavior in our application. Proper cleanup ensures that resources are freed up when no longer needed, leading to better performance and preventing potential bugs.

#Comparing useState and useEffect

Understanding the useEffect Hook is crucial, but it's equally important to grasp how it works alongside useState. Both Hooks are essential for managing state and side effects in functional components.

The useState Hook allows you to add a state to your functional components. It returns a stateful value and a function to update it. The useEffect Hook, as explained, handles side effects.

Here’s an example using both Hooks:

import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Counter;

In this example, useState initializes count to 0, and setCount updates the count when the button is clicked. Each call to setCount triggers a re-render, ensuring the UI reflects the current state.

Also, the useEffect is used to update the document title whenever the count changes. The dependency array [count] ensures that this effect runs only when count updates.

This is why you need to specify the right dependencies when using useEffect. If you don't provide the correct dependencies, the effect might not run as it should or run too often, causing unnecessary performance issues. For instance, omitting count from the dependency array would result in the useEffect not re-running when count changes, leading to the document title not updating correctly.

Conversely, if you include too many dependencies or fail to specify them accurately, the effect might run more frequently than necessary, leading to performance barriers due to repeated execution of potentially expensive operations.

#Rules of Hooks

Hooks in React follow two rules to ensure consistency and avoid common pitfalls; these guidelines help maintain predictable behavior in your components. Here are the rules:

1) Only call Hooks at the top level: Don’t call Hooks inside loops, conditions, or nested functions. Hooks must be called at the top level of your component to ensure they execute in the same order every time a component renders. This guarantees that the state and effect management logic remains consistent.

import { useState, useEffect } from 'react';
function ExampleComponent() {
const [value, setValue] = useState(0);
// Correct: Hooks are called at the top level
useEffect(() => {
console.log('Effect runs');
}, []);
// Incorrect: Hooks inside a conditional
if (value > 0) {
useEffect(() => {
console.log('This will cause issues');
}, []);
}
return <div>Value: {value}</div>;
}

2) Only call Hooks from React functions: Hooks can only be called from functional components or other custom Hooks. This ensures that Hooks are only used in the correct context and not in arbitrary JavaScript functions.

#useEffect vs. useLayoutEffect

Understanding the difference between useEffect and useLayoutEffect is key for optimizing your React application's performance and behavior.

Both Hooks allow you to perform side effects in functional components, but they differ in timing and purpose. Let's break it down:

  • useEffect runs after the browser has painted the screen. "Painting the screen" refers to the process where the browser renders the visual representation of the UI. This makes useEffect ideal for tasks that don't need to block the rendering process, such as fetching data from an API, logging, or setting up subscriptions.
  • useLayoutEffect runs before the browser paints the screen. This means it can synchronously measure and manipulate the DOM before the user sees any changes. Use it for tasks that need to be performed immediately before the screen update, such as measuring the layout, adjusting styles, or modifying DOM elements based on calculations.

Editor's Note

Despite their differences in timing, both Hooks share the same syntax.

Here's a practical example to illustrate their differences:

import { useState, useEffect, useLayoutEffect } from 'react';
function LayoutEffectExample() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("useEffect:", count);
}, [count]);
useLayoutEffect(() => {
console.log("useLayoutEffect:", count);
}, [count]);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<p>Count: {count}</p>
<div
style={{
width: `${count * 50}px`,
height: `${count * 50}px`,
background: "red"
}}
></div>
</div>
);
}
export default LayoutEffectExample;

In this example:

  1. The useEffect logs the count value after the browser paints the screen. This might cause a slight delay in updates being reflected in the console.
  2. The useLayoutEffect logs the count value before the paint, ensuring that layout changes are reflected immediately in the console.

When you click the "Increment" button, the console logs from useLayoutEffect appear before those from useEffect. This illustrates the synchronous nature of useLayoutEffect compared to the asynchronous nature of useEffect.

Key takeaways:

  • Use useEffect for side effects that don't require immediate DOM updates. This includes data fetching, setting up subscriptions, and other non-blocking operations.
  • Use useLayoutEffect for side effects that need to occur before the DOM is updated, such as layout measurements and adjustments.
  • Be mindful of performance, as useLayoutEffect can block the browser from painting, so use it sparingly and only when necessary.

#React Server Components and UseEffect

Earlier in this article, we mentioned that with the introduction of server components, Hooks can only be used in client components. This distinction is important to understand as it impacts how and where you can use useEffect in your React applications.

Client components are rendered on the client side, which means they can leverage the full capabilities of the browser, including state management and lifecycle methods provided by Hooks like useEffect.

Server components, on the other hand, are rendered on the server. This means they cannot access the browser’s capabilities and cannot use Hooks like useEffect. Server components are meant to handle tasks that can be completed during server-side rendering, such as fetching data and rendering the initial HTML.

A common pattern is to use server components to fetch and render initial data, and then enhance interactivity with Client components.

#Wrapping up

You've comprehensively covered the useEffect Hook, from its basic usage to more advanced concepts like dependencies and clean up functions.

Understanding how useEffect works with useState to manage side effects and state within functional components helps ensure your UI reflect the latest data and is free of unnecessary renders.

Sign up for a free-forever developer account at Hygraph to build more sophisticated applications. Happy coding!

Blog Author

Joel Olawanle

Joel Olawanle

Joel Olawanle is a Frontend Engineer and Technical writer based in Nigeria who is interested in making the web accessible to everyone by always looking for ways to give back to the tech community. He has a love for community building and open source.