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 mountsthis.fetchData();}componentDidUpdate(prevProps) {// Refetch data when props changeif (this.props.someValue !== prevProps.someValue) {this.fetchData();}}componentWillUnmount() {// Cleanup before unmountingthis.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
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:
- A callback function that contains the side effect logic.
- 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.
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 logicreturn () => {// 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
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 {idslugtitleexcerptcoverImage {url}publishedAtauthor {namepicture {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
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 stableuseEffect(() => {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 updatesconst 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
#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 functionreturn () => {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 functionreturn () => {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 leveluseEffect(() => {console.log('Effect runs');}, []);// Incorrect: Hooks inside a conditionalif (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 makesuseEffect
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
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><divstyle={{width: `${count * 50}px`,height: `${count * 50}px`,background: "red"}}></div></div>);}export default LayoutEffectExample;
In this example:
- 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. - 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!