Learn and Teach Coding

Fetch Users from API

Fetch and display a list of users from a REST API with loading and error states.

ReactBeginnerapifetchuseEffect

Live preview

Users

Code walkthrough

The bread-and-butter of real apps: call an API, handle the loading and error states, then render the result. This version uses useEffect and the fetch API with an AbortController for clean cancellation. The data comes from a same-origin static endpoint (/api/users.json), so there's no third-party dependency — swap the URL for any real REST API and the rest of the code is unchanged. Use View full code above for the complete component.

1. Model the request as a status

Instead of separate loading/error booleans, a single status union makes the states mutually exclusive — you can never be "loading" and "error" at once.

const [users, setUsers] = useState<User[]>([]);
const [status, setStatus] = useState<"loading" | "success" | "error">("loading");

2. The fetch, with cancellation

load accepts an optional AbortSignal. We check res.ok, parse JSON, and swallow the AbortError that fires when a request is cancelled (that's expected, not a real failure).

const load = useCallback(async (signal?: AbortSignal) => {
  try {
    const res = await fetch("/api/users.json", { signal });
    if (!res.ok) throw new Error(`Request failed: ${res.status}`);
    setUsers(await res.json());
    setStatus("success");
  } catch (err) {
    if ((err as Error).name !== "AbortError") setStatus("error");
  }
}, []);

3. Fetch on mount, abort on unmount

The effect creates a controller, kicks off the request, and returns controller.abort() for cleanup — so navigating away mid-flight never sets state on an unmounted component.

useEffect(() => {
  const controller = new AbortController();
  load(controller.signal);
  return () => controller.abort();
}, [load]);

Always clean up in-flight requests. Without the abort, a slow response that resolves after unmount triggers a state update on a gone component.

4. Rendering each state

The JSX branches on status: a skeleton list while loading, a retry card on error, and the user list on success. Open the full code to see the skeleton and list markup.

Related examples

JSIntermediate

Debounced Search in JavaScript

Implement a debounced search input with vanilla JavaScript.

View example