Home / Free Tutorials

Practical Learning Guide

Handling API Data in React

Build explicit loading, error, empty and success states with cleanup for changing requests

This self-study guide explains the topic and provides a practical starting point. For trainer-led learning, review the related course and request the current syllabus and batch details.

Read the Study Plan

Define the response before rendering

This component expects an array of objects with unique string IDs and string labels. Write that contract down before mapping data into list elements. Treat an unexpected response shape as a visible error rather than letting a property-access exception appear deep in rendering. In a real application, choose validation that matches your actual endpoint and domain.

Separate four interface states

Loading means the current request is not finished. Error means it failed or returned invalid data. An empty array is a successful response with no records, while a non-empty array can render the list. The example keeps those states distinct, clears old rows when starting a new request and supplies a status or alert message where appropriate.

Check the HTTP result and parsing

The fetch promise does not automatically reject every unsuccessful HTTP status. Check response.ok before reading the JSON body, then validate the parsed value. The component shows HTTP status errors separately from its Unexpected data message. JSON parsing failures also enter the error path; avoid showing raw server internals or personal data in a production error message.

Clean up when the request changes

The Effect depends on url and request. Cleanup marks its result inactive and aborts its controller before a replacement Effect runs or when the component unmounts. The active flag also prevents an old result from changing state if the underlying request ignores cancellation. Keep an injected request function stable; recreating it on every parent render would repeatedly restart this Effect.

Integrate the component deliberately

Save the code as orders.jsx inside an existing React project, import Orders and render it with your actual read-only API URL. It uses JSX and requires your normal React build setup; it is not a script to paste directly into a plain HTML page. No /api/orders backend is created by this lesson. Keep credentials and authorization checks in their appropriate application layers.

Practice without an external service

The companion orders-demo.jsx uses local synthetic Response objects and a scenario selector. Use it as your app entry alongside orders.jsx to practise without sending network requests. It mounts to an element with id root. The slow scenario intentionally ignores abort to test the separate stale-result guard. The normal component still defaults to the browser's fetch when no request prop is supplied.

Run a focused acceptance checklist

Check that success renders Sample order, empty renders No orders found, HTTP error shows HTTP 503 and malformed data shows Unexpected data. Switch from slow to empty before the old result arrives; the empty state must remain after the slow response completes. Test cleanup under Strict Mode as well as when replacing the URL.

Understand the limits of this example

This is a small client-side learning example, not a general data-loading architecture. It has no cache, pagination, automatic retries, server rendering or production authentication. A framework's supported data-loading approach or a dedicated client data library may suit a larger application. The exercise is to explain state and cleanup, then choose the appropriate approach for the actual project.

orders.jsx — React component with a defined response contract

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

export function Orders({ url, request = fetch }) {
  const [result, setResult] = useState({ status: "loading", rows: [], error: "" });
  useEffect(() => {
    const controller = new AbortController();
    let active = true;
    setResult({ status: "loading", rows: [], error: "" });
    async function load() {
      try {
        const response = await request(url, { signal: controller.signal });
        if (!response.ok) throw new Error("HTTP " + response.status);
        const rows = await response.json();
        if (!Array.isArray(rows) || rows.some(row =>
          !row || typeof row.id !== "string" || typeof row.label !== "string"
        ) || new Set(rows.map(row => row.id)).size !== rows.length) {
          throw new Error("Unexpected data");
        }
        if (active) setResult({ status: "ready", rows, error: "" });
      } catch (error) {
        if (active && error.name !== "AbortError") {
          setResult({ status: "error", rows: [], error: error.message });
        }
      }
    }
    load();
    return () => { active = false; controller.abort(); };
  }, [url, request]);

  if (result.status === "loading") return <p role="status">Loading orders...</p>;
  if (result.status === "error") return <p role="alert">{result.error}</p>;
  if (result.rows.length === 0) return <p role="status">No orders found.</p>;
  return <ul>{result.rows.map(row => <li key={row.id}>{row.label}</li>)}</ul>;
}

Expected result

Success: Sample order
Empty: No orders found.
HTTP failure: HTTP 503
Malformed response: Unexpected data
Slow followed by empty: remains empty after the old result completes.

Further reading

Check your progress

Keep your working files, test cases and notes together. Explain one design decision, demonstrate a failure case and list what you would improve next. These concrete outputs make it easier to identify the next skill to practice.