Chapter 13 of 20 · React for Beginners — Complete Free Tutorial

Fetching Data from an API

Almost every interesting application in the React for Beginners series loads data from somewhere else: a REST endpoint, a search service, your own backend. Fetching looks simple — call fetch, put the result in state — but the details (loading states, errors, cancellation, race conditions) are what separate a demo from a working feature. This chapter builds the pattern you will reuse everywhere.

The three states of remote data

Whenever data comes from the network, the UI must handle three mutually exclusive conditions — plus a fourth that beginners forget:

StateTriggerWhat to show
LoadingRequest started, no answer yetSpinner, skeleton, "Loading…"
ErrorNetwork failure or non-2xx statusMessage and a retry affordance
SuccessData arrived and parsedThe content
EmptySuccess, but zero itemsA helpful empty state — not a blank screen

Model them explicitly with state. A single status string plus a data value keeps impossible combinations (loading and showing data) out of your component.

The standard effect

Fetch inside useEffect, list the trigger values as dependencies, and update state when the response arrives:

import { useEffect, useState } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [status, setStatus] = useState('loading');  // loading | error | ready

  useEffect(() => {
    async function load() {
      try {
        const res = await fetch('https://jsonplaceholder.typicode.com/users');
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = await res.json();
        setUsers(json);
        setStatus('ready');
      } catch (err) {
        setStatus('error');
      }
    }
    load();
  }, []);

  if (status === 'loading') return <p role="status">Loading users…</p>;
  if (status === 'error') return <p className="error">Could not load users.</p>;
  return (
    <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
  );
}

Expected output: briefly "Loading users…", then the list of names — or an error message if the request fails. Every branch of the UI is covered, so the screen is never mysteriously blank.

Cancellation: the race condition

If the effect can re-run (different id in the URL, for example), two requests may be in flight at once — and the slower one can arrive last and overwrite fresher data. Modern fetch solves this with an AbortController returned as the effect's cleanup:

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

  async function loadPost() {
    setStatus('loading');
    try {
      const res = await fetch(`/api/posts/${postId}`, {
        signal: controller.signal,
      });
      const json = await res.json();
      setPost(json);
      setStatus('ready');
    } catch (err) {
      if (err.name !== 'AbortError') setStatus('error');
    }
  }
  loadPost();

  return () => controller.abort();   // cancels the stale request
}, [postId]);

Expected output: switching rapidly between posts leaves only the last request's result on screen; aborted requests are silently ignored instead of surfacing as errors. This is the same cancellation idea you may know from the Fetch chapter, now wired into React's lifecycle.

React 18/19 semantics

Two behaviours worth knowing for 2026-era React:

  • Effects run once per mount in development — StrictMode mounts, unmounts and remounts to verify your cleanup; a correctly paired effect fetches twice in dev and once in production, with no leaked requests.
  • State updates after unmount are ignored, not fatal — React no longer warns about setting state on an unmounted component, but cancellation still matters for bandwidth and race conditions.
  • async does not belong directly in the effect callback — the callback must return nothing (or your cleanup), so declare an inner async function and call it, exactly as above.

Loading and retrying

A polished loader is more than a spinner. Keep the previous data visible while refetching (avoid layout jumps), disable the button that triggered the request, and offer a retry that simply increments a refreshKey in the dependency array — re-running the effect without new props:

const [refreshKey, setRefreshKey] = useState(0);
useEffect(() => { /* fetch using refreshKey as a trigger */ }, [refreshKey]);

return (
  <>
    {status === 'error' && (
      <button onClick={() => setRefreshKey((k) => k + 1)}>Try again</button>
    )}
  </>
);

Expected output: after a failure the user clicks "Try again" and the same effect re-runs — no reload, no duplicated fetch code.

Beyond raw fetch

This pattern is the foundation. As applications grow, libraries such as TanStack Query or SWR add caching, background refetching and request de-duplication on top of exactly these states. Learn the manual version first — you will then understand what those tools automate.

Common mistakes

  • Fetching without a loading state — the UI flashes empty and looks broken on slow connections.
  • Ignoring non-2xx responses — fetch only rejects on network failure; a 404 or 500 still needs an explicit res.ok check.
  • Missing or wrong dependencies — fetching a record by id with [] shows the first record forever.
  • Letting stale responses win — without cancellation, an old request can overwrite newer data.
  • Calling setState outside any guard in a loop — update once per request, after validation, and only for the request that is still relevant.

Practice checklist

  1. Build the user list with all four states, including the empty case (use a URL that returns []).
  2. Log the network tab: confirm requests are made once per mount and once per dependency change.
  3. Add AbortController cancellation and test it by switching records quickly.
  4. Implement the retry button via a refresh key.
  5. Simulate an error (typo the URL) and verify the error branch renders with a working retry.

Data now flows from the server into your UI. To keep it organised as the app grows, reshape your components with Component Composition and Children.

Quiz: Fetching Data from an API

5 questions · 70% to pass

Frequently Asked Questions

React is a JavaScript library for building user interfaces from reusable components. It manages how your UI updates when data changes, so you describe what the screen should look like and React performs the efficient DOM updates. It is one of the most in-demand frontend skills and powers products from Netflix to Instagram, which makes it an excellent next step after core JavaScript.

You should be comfortable with functions, arrays and objects (map, filter, spread), destructuring, modules and basic async/await. React itself is written in plain JavaScript — JSX is a syntax layer on top. If any of those feel shaky, revisit the JavaScript fundamentals first and this series will go much faster.

React is a library focused only on the UI layer — you choose your own router, data layer and tooling. Angular is a full framework that ships routing, forms, HTTP client and testing conventions as one opinionated package. React's flexibility and huge ecosystem make it the common choice for teams that want to compose their own stack.

A component is a reusable function that returns UI. It receives input through props, keeps private data in state, and can render other components. Pages are composed by nesting small components (navbar, card list, footer) instead of one giant file, which keeps code testable and easy to change.

JSX is a syntax extension that looks like HTML but compiles to React's create() calls — it is a description of UI, not markup the browser parses. Small differences matter: class becomes className, style takes an object, and you embed JavaScript expressions inside curly braces. Once compiled, React turns that description into real DOM nodes.

Props are inputs passed from a parent — they are read-only inside the child, like function arguments. State is private, mutable data owned by a component; updating it triggers a re-render. The rule of thumb: values that come from outside the component are props; values that change over the component's lifetime are state.

Rendering means calling your component function to produce a description of the UI for the current state and props. It does not always mean DOM work — React compares the new description with the previous one (reconciliation) and applies only the differences to the real DOM. This is why updating state 100 times does not mean 100 full page rebuilds.

An effect runs after the component has committed to the screen — after render, not during it. You list dependencies in an array, and React re-runs the effect only when one of them changes. If the effect returns a cleanup function, React runs it before the next re-run and on unmount, which is where you cancel timers, abort fetches and remove listeners.

Use Vite. Create React App is effectively in maintenance mode, while Vite starts instantly (native ES modules) and builds fast, and it is the default choice for new React projects. This series uses Vite for every example — you only need Node.js installed to follow along.

React apps compile to static files (HTML, CSS and JavaScript), so any static host works: Netlify, Vercel, GitHub Pages or Cloudflare Pages. Run the production build, upload the dist folder, and configure a single-page-application fallback so unknown paths load index.html — routing chapters explain why that fallback is required.