Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 36

useEffect for Data Fetching on Mount

Recommended resources

Introduction

In our last lesson, you mastered conditional rendering, learning how to show and hide UI elements based on state. This is the exact skill you need to create user experiences that feel responsive and informative, especially when dealing with data that isn't instantly available. You know from your UX work that showing a loading indicator or a clear error message is crucial for a good user journey.

This lesson connects that concept to a core React capability: fetching data from an API. We'll introduce the useEffect hook, which allows your components to perform "side effects"—actions that interact with the world outside of React, like making an API request. You will learn the standard, robust pattern for fetching data when a component first mounts, managing loading and error states along the way. Most importantly, you will learn to recognize what correct, professional-grade data fetching code looks like, empowering you to effectively supervise and fix the code generated by AI tools like Cursor.

Mastering `useEffect` for data fetching involves understanding a few key patterns and avoiding common pitfalls, which is essential for building reliable applications.

Side Effects and the useEffect Hook

In React, a component's main job is to transform props and state into JSX. A "side effect" is anything your component does that affects something outside of its own scope. Common examples include:

  • Fetching data from a server (an API call).
  • Setting up a subscription to a WebSocket.
  • Manually changing the DOM (e.g., to focus an input field).

React provides a special hook to manage these side effects: useEffect. The key thing to remember is that useEffect runs after your component has rendered to the screen.

The basic syntax is:

useEffect(() => {
  // Your side effect code goes here.
}, [dependencies]);

The second argument, the dependency array, is crucial. It tells React when to re-run the effect:

  • [] (an empty array): The effect runs only once, after the component's initial render. This is perfect for fetching initial data when a component first appears or "mounts".
  • [propA, stateB]: The effect runs after the initial render, and then again any time propA or stateB changes.
  • No array: The effect runs after every single render. This almost always leads to bugs (like infinite loops) and should be avoided.

The Anatomy of a Data Fetching Effect

Let's build the complete, robust pattern for fetching data on component mount. This pattern directly implements the state transitions (Loading → Success/Error) you would design in Figma.

The process involves combining three hooks: useState (for managing data, loading, and error states), useEffect (to perform the fetch), and the conditional rendering logic you learned in the last lesson.

This flowchart illustrates the lifecycle of a component and where `useEffect` fits in. For our purposes, we're focusing on the "Mount" phase, where the effect callback runs once to fetch initial data.

Here is a breakdown of the standard pattern:

  1. Three States, Three useState Hooks: You need to track the data itself, whether the request is in progress, and if an error occurred.

    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    

    We initialize loading to true because the fetch will begin immediately on mount.

  2. The useEffect Structure: We define an async function inside the useEffect callback. You cannot make the callback itself async.

    useEffect(() => {
      const fetchData = async () => {
        // Fetching logic will go here
      };
    
      fetchData();
    }, []); // Empty array means run once on mount
    
  3. The try/catch Block: To handle both success and failure gracefully, we wrap our fetching logic in a try/catch block.

    try {
      const response = await fetch('api/url');
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      const jsonData = await response.json();
      setData(jsonData);
    } catch (error) {
      setError(error);
    } finally {
      setLoading(false);
    }
    
    • The try block attempts the fetch. If it succeeds, it updates the data state.
    • The catch block executes if any error occurs (e.g., network down, server error), updating the error state.
    • The finally block always runs, whether the fetch succeeded or failed. It's the perfect place to set loading to false.
  4. Conditional Rendering: Finally, you use the state variables to render the correct UI.

    if (loading) {
      return <div>Loading...</div>;
    }
    if (error) {
      return <div>Error: {error.message}</div>;
    }
    return (
      // Your success UI that uses the `data`
    );
    

The following article provides a clear, complete example of this entire pattern.

How to Handle API Calls with useEffect

This article from OneUptime demonstrates the complete, modern pattern for fetching data in a React component, including state management for loading and errors.

Read the section from the beginning down to the end of the first code example. Pay close attention to how the UserProfile component uses useState for user, loading, and error, and how the useEffect hook orchestrates the API call and state updates. Notice how the final return statements use conditional rendering to handle each state.


Supervising AI: Recognizing and Fixing Common useEffect Mistakes

As a Design Engineer using Vibe Coding, your most important job is not to write this code from scratch, but to supervise it. AI tools can generate data-fetching components quickly, but they often produce code that is buggy, inefficient, or fragile. Here are the critical mistakes to watch for.

Mistake 1: The Infinite Loop (Missing Dependency Array)

If the AI forgets the [] dependency array, the useEffect will run after every render. Since setting state with setData causes a re-render, this creates an infinite loop of fetching and re-rendering.

  • What to look for: A useEffect that fetches data but has no second argument.
  • How to fix: Add an empty array [] as the second argument to useEffect.

The following article explains this classic mistake and its simple solution very clearly.

How to fetch data with React Hooks

This guide by Robin Wieruch is a classic resource on data fetching in React. This particular section masterfully explains the most common useEffect pitfall.

Read the text starting from the paragraph that discusses the loop. The author clearly explains why leaving out the dependency array causes an infinite loop and how adding [] solves it.

Mistake 2: Unhandled Race Conditions and Missing Cleanup

This is a more subtle but critical bug. Imagine a search component that refetches data as the user types.

  1. User types "react". A fetch request begins.
  2. Before that request finishes, the user quickly types "nextjs". A second fetch request begins.
  3. The "nextjs" request is fast and returns first. The UI updates correctly.
  4. Then, the original "react" request finally finishes. It overwrites the UI with the now-stale "react" data.

The UI is now showing the wrong results. This is a race condition. The solution is to cancel the previous request before starting a new one. This is done using useEffect's cleanup function and a tool called AbortController.

The cleanup function is a function that you can return from your useEffect callback. React will run this function just before it runs the effect again, or when the component is unmounted.

Fetching Data in React - Complete Tutorial

This tutorial from Cosden Solutions provides an exceptionally clear, step-by-step explanation of race conditions and how to solve them using AbortController.

Watch the section from "one thing that you really have to think about". The presenter does an excellent job of: Explaining what a race condition is with a practical example (00:15:31 - 00:21:07). Demonstrating the AbortController pattern to cancel the previous request (00:21:07 - 00:25:02). Showing how to handle the specific AbortError so it doesn't trigger your error UI (00:25:02 - 00:27:01).

When supervising AI code, always check if it includes this cleanup logic, especially for effects that can run multiple times. If it's missing, you now know how to add it.

Mistake 3: "Cannot read properties of null"

This error happens when your component tries to render data.name before the data has been fetched, when data is still null or undefined.

  • What to look for: Accessing properties on a state variable that is initialized to null without any protection.
  • How to fix: Ensure your conditional rendering is correct. The if (loading) or if (!data) check must come before any code that tries to access properties on data.

This short video explains the problem and the clean solution using a loading state.

All 12 useState & useEffect Mistakes Junior React Developers Still Make in 2025

This ByteGrad video highlights common React mistakes. This specific segment is perfect for understanding why you get errors when rendering fetched data.

Watch the segment on "initializing state with an object". It clearly shows the error that occurs and demonstrates how implementing a proper loading state is the clean, correct solution.


Your Vibe Coding Prompt

Now you can write a much more powerful and precise prompt for Cursor. Instead of just "fetch user data," you can specify the entire robust pattern.

Weak Prompt:

"Create a component to fetch and display a user's profile."

Strong, Supervisable Prompt:

"Create a React component named UserProfile that accepts a userId prop.

  1. On component mount, it should fetch user data from the API endpoint https://jsonplaceholder.typicode.com/users/${userId}.
  2. Use useState to manage three states: user (for the data), loading (boolean), and error (for any errors).
  3. Implement conditional rendering:
    • If loading is true, show a div with the text "Loading profile...".
    • If an error occurs, show a div with the error message.
    • If successful, display the user's name, email, and company name.
  4. Use useEffect's cleanup function and an AbortController to cancel the fetch request if the component unmounts before the request completes. Ensure you handle the 'AbortError' so it's not treated as a real error."

This detailed prompt forces the AI to generate professional-grade code that you can then confidently review and approve.


Conclusion

You've now learned one of the most fundamental patterns in React development: fetching data with useEffect. You understand not just the "happy path," but also how to handle loading states, errors, and complex issues like race conditions.

Key Takeaways:

  • useEffect is the hook for managing side effects, like API calls, which run after the component renders.
  • The dependency array ([]) is key to ensuring a fetch effect runs only once on mount.
  • A robust data-fetching component manages three states: data, loading, and error.
  • A useEffect cleanup function with AbortController is the standard way to prevent race conditions by canceling stale requests.
  • Supervising AI means looking for these specific patterns and pitfalls: missing dependency arrays, lack of cleanup, and improper handling of initial null state.

In our next lesson, we'll shift gears slightly. Now that your components are becoming more complex, managing your codebase becomes more important. We'll dive into Git branching, a fundamental skill that allows you to work on new features and experiments in isolation, without risking the stability of your main project.