Skip to main content

The `useEffect` Hook for Side Effects (Part 2) #66

📖 Introduction

In the previous article, we learned that useEffect is React's dedicated tool for handling side effects. We saw how to update the DOM and manage timers. Now, we'll tackle the most common and crucial side effect in modern web applications: fetching data from an API.

This article will guide you through building a component that fetches external data, manages loading and error states, and displays the result, providing a complete pattern you can use in your own projects.


📚 Prerequisites

To get the most from this article, you should understand:

  • The basic concept of useEffect and side effects.
  • The useState hook.
  • Basic JavaScript async/await syntax and the fetch API.

🎯 Article Outline: What You'll Master

By the end of this article, you will have built a robust data-fetching component and learned:

  • The Data-Fetching Lifecycle: How to manage the three states of an API request: loading, success, and error.
  • Fetching on Mount: Using useEffect with an empty dependency array ([]) to fetch data when a component first renders.
  • Conditional Rendering: How to show a loading indicator, an error message, or the final data based on the state of the request.
  • Best Practices: How to properly structure your data-fetching logic within a component.

🧠 Section 1: The Three States of a Network Request

Before writing code, it's essential to understand the lifecycle of any data request. It can be in one of three states:

  1. Loading: The request has been sent, and we are waiting for a response.
  2. Success: We have received the data successfully.
  3. Error: The request failed for some reason (e.g., network error, server error).

Our component needs to track which of these three states it's in so it can render the appropriate UI. We will use useState to manage this.

const [data, setData] = useState(null);     // For the success state
const [loading, setLoading] = useState(true); // For the loading state
const [error, setError] = useState(null); // For the error state

💻 Section 2: Fetching Data with useEffect

We want to fetch our data as soon as the component is placed on the screen (i.e., when it "mounts"). The useEffect hook is perfect for this. To make an effect run only once on mount, we provide an empty array [] as the second argument.

Here is the complete component. We'll use the free JSONPlaceholder API for our example.

// DataFetcher.jsx

import React, { useState, useEffect } from 'react';

function DataFetcher() {
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
// The empty dependency array `[]` means this effect will run only once,
// right after the component mounts.
const fetchPost = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setPost(data);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
};

fetchPost();
}, []);

// Conditional rendering based on the state
if (loading) {
return <div>Loading post...</div>;
}

if (error) {
return <div>Error: {error.message}</div>;
}

return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
}

export default DataFetcher;

🛠️ Section 3: Step-by-Step Code Breakdown

Let's break down exactly what's happening in our DataFetcher component.

  1. State Initialization:

    • post is initialized to null because we have no data yet.
    • loading is initialized to true because we want to show the loading message immediately when the component renders for the first time.
    • error is initialized to null.
  2. The useEffect Hook:

    • We define an async function fetchPost inside the effect.
    • try...catch...finally block: This is a robust way to handle promises.
      • try: We await the fetch call. If the response is not "ok" (e.g., a 404 or 500 error), we manually throw an error to be caught by the catch block. If it is ok, we parse the JSON and call setPost to store our data.
      • catch: If any error occurs in the try block, it's caught here, and we call setError to save the error object.
      • finally: This block always runs, whether the request succeeded or failed. It's the perfect place to call setLoading(false), since the loading process is now over.
    • We call fetchPost() to start the process.
    • The empty dependency array [] at the end is critical. It tells React: "Only run this effect once, after the initial render." Without it, the effect would run after every render, causing an infinite loop of fetching and re-rendering.
  3. Conditional Rendering:

    • The component uses if statements to check the loading and error states first.
    • If loading is true, it returns the loading message.
    • If error is not null, it returns the error message.
    • Only if both loading and error are falsy does it proceed to render the final UI with the fetched data.

💡 Conclusion & Key Takeaways

You have now built a complete, robust data-fetching component in React. This pattern of using useState to manage the three states of a request and useEffect to perform the fetch on mount is one of the most common and important patterns in all of React.

Let's distill the key lessons:

  • The Three-State Pattern: Always account for loading, success, and error states when dealing with asynchronous operations.
  • Fetch on Mount: Use useEffect with an empty dependency array [] to run a side effect exactly once when the component is first rendered.
  • Render Conditionally: Use the state variables to control what the user sees at each stage of the data-fetching lifecycle.

This foundational knowledge is the gateway to building dynamic applications that can interact with any API on the web.


➡️ Next Steps

We've seen how to run an effect once, but what if we want to re-run an effect only when a specific piece of data changes? In the next article, "The Dependency Array Explained (Part 1)", we will take a deep dive into the second argument of useEffect, unlocking the full power of the hook and giving us precise control over our side effects.

The journey continues, and the path ahead is filled with dynamic possibilities. Keep building!