Course topics

By WebNest Studio Engineering Team

React Tutorial

Fetching Data and API Integration in React

Most real applications need to load data from a server rather than hardcoding it. In a function component, this is done inside a useEffect hook, since fetching data is a side effect that shouldn't happen during rendering itself. The two most common tools for making the request are the browser's built-in fetch API and the popular third-party library axios.

Beyond just calling an API, a properly integrated data-fetching component also needs to manage three distinct states: loading (the request is in flight), success (data arrived), and error (the request failed) — and render appropriate UI for each.

Why it matters

Almost every React application gets its data from an API, and doing that well means handling three states, not one: loading, success and failure. Skipping the loading or error state gives a blank screen when the network is slow or the server fails, which is exactly when users most need feedback.

Fetching Data with useEffect

The fetch call is placed inside a useEffect with an appropriate dependency array — an empty array ([]) means "run once, after the first render," while including a value like userId means "re-run whenever userId changes." Inside the effect, you typically set a loading flag to true, perform the request, then update state with the result or the error, and finally set loading back to false.

Managing Loading and Error State

A robust data-fetching component keeps at least three pieces of state: the data itself (initially null or an empty value), a loading boolean (initially true), and an error value (initially null). The component's JSX then branches on these: show a spinner or "Loading..." message while loading is true, show an error message if error is set, and otherwise render the data.

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

Cleanup and Avoiding Race Conditions

If a component can unmount or its dependencies can change before a request finishes, setting state after that point can trigger warnings or bugs. The standard fix is to track whether the effect is still "current" — either with an AbortController (works with fetch) to cancel the in-flight request, or a boolean flag checked before calling setState in the effect's cleanup function.

Examples

Fetching a user list with loading and error states, using fetch

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

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    async function loadUsers() {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/users', {
          signal: controller.signal,
        });
        if (!response.ok) {
          throw new Error(`Request failed with status ${response.status}`);
        }
        const json = await response.json();
        setUsers(json);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }

    loadUsers();
    return () => controller.abort(); // cancel if the component unmounts first
  }, []);

  if (loading) return <p>Loading users...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default UserList;
Output
Initially renders "Loading users...". Once the fetch resolves, it renders a bulleted list of user names from the API; if the request fails (network error or non-2xx status), it renders "Error: <message>" instead of the list.

The same request using axios, which parses JSON automatically

JavaScript
import { useEffect, useState } from 'react';
import axios from 'axios';

function PostDetails({ postId }) {
  const [post, setPost] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;
    axios
      .get(`https://jsonplaceholder.typicode.com/posts/${postId}`)
      .then((response) => {
        if (isMounted) setPost(response.data);
      })
      .catch((err) => {
        if (isMounted) setError(err.message);
      });
    return () => {
      isMounted = false;
    };
  }, [postId]);

  if (error) return <p>Error: {error}</p>;
  if (!post) return <p>Loading post...</p>;

  return <h2>{post.title}</h2>;
}

export default PostDetails;
Output
Shows "Loading post..." then replaces it with the post title once axios resolves. If postId changes, the effect re-runs and fetches the new post; the isMounted flag prevents a state update if the component unmounted first.

Common Mistakes

  • Calling fetch directly inside the component body instead of inside useEffect, which causes an infinite loop of requests on every render.
  • Forgetting to check response.ok with fetch — unlike axios, fetch does not reject the promise on a 404 or 500 status.
  • Omitting the effect's cleanup/cancellation, causing "state update on an unmounted component" warnings or stale data overwriting newer data.
  • Leaving the dependency array empty when the fetch actually depends on a prop or state value like an id, so the data never refreshes when it should.

Exercise

Do this in src/App.jsx of a Vite + React project. Fetch the list of users from https://jsonplaceholder.typicode.com/users when the component mounts. Show "Loading" while waiting, an error message if the request fails or returns a bad status, and otherwise the users' names in a list. Cancel the request if the component is removed.

Expected result: "Loading" appears briefly, then a list of ten names. With the network turned off, an error message is shown instead.

JavaScript · starter code
import { useEffect, useState } from "react";

export default function App() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // TODO: fetch the users with an AbortController, check response.ok,
    //       store the result or the error, and clear the loading flag
    // TODO: return a clean-up that aborts the request
  }, []);

  // TODO: render the loading state, the error state, or the list
  return null;
}
Show hints
  • Pass { signal: controller.signal } to fetch and call controller.abort() in the clean-up.
  • fetch does not reject on a 404 or 500, so check response.ok and throw. Ignore the error whose name is AbortError.
Show solution
JavaScript · solution
import { useEffect, useState } from "react";

export default function App() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch("https://jsonplaceholder.typicode.com/users", { signal: controller.signal })
      .then((response) => {
        if (!response.ok) {
          throw new Error("Request failed with status " + response.status);
        }
        return response.json();
      })
      .then((data) => {
        setUsers(data);
        setLoading(false);
      })
      .catch((problem) => {
        if (problem.name === "AbortError") return;
        setError(problem.message);
        setLoading(false);
      });

    return () => controller.abort();
  }, []);

  if (loading) return <p>Loading</p>;
  if (error) return <p>Something went wrong: {error}</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Quick quiz

1. Which three states should a component that loads data handle?

  • A. Start, middle, end
  • B. Loading, success and error
  • C. Open, closed, pending
  • D. Get, post, delete
Show answer

B. Each state needs something sensible on screen.

2. Why can the effect callback itself not be an async function?

  • A. An async function returns a promise, and React expects an effect to return nothing or a clean-up function
  • B. async is not allowed in React
  • C. It would run twice
  • D. It would block rendering
Show answer

A. Define an async function inside the effect and call it.

3. What is a race condition when fetching in an effect?

  • A. Two components rendering at once
  • B. A request that is too fast
  • C. A request with no headers
  • D. An earlier, slower response arriving after a later one and overwriting the newer data
Show answer

D. Aborting the previous request in the clean-up, or ignoring its result, prevents it.

Interview questions

How do you fetch data in a React component?

Show answer

In a useEffect that runs when the inputs of the request change: set a loading flag, make the request, check the status, store the data or the error in state, and return a clean-up that aborts the request so that an outdated response cannot update the component. In practice, most applications use a data-fetching library such as TanStack Query or SWR, or the data loading of a framework, which add caching, retries and deduplication.

Why use a library such as TanStack Query instead of fetch in useEffect?

Show answer

Fetching by hand means rewriting loading and error state, cancellation and race handling for every request, and it gives no caching: the same data is requested again by every component that needs it and on every visit. A query library caches responses by key, shares them between components, refetches in the background when data may be stale, retries failures and handles pagination, with far less code.

Key Points to Remember

  • Data fetching belongs inside useEffect, not directly in the render body, because it is a side effect.
  • Track loading, data, and error as separate state values and render UI conditionally based on them.
  • fetch resolves even on HTTP error responses (check response.ok); axios rejects the promise automatically and parses JSON for you.
  • Cancel or ignore stale requests on cleanup to avoid race conditions and updates after unmount.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of plain HTML, CSS and JavaScript. JSX needs a React project on your computer. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering