React
API Fetch
Button Click
JavaScript
Web Development

React - Fetch from external API function on button click

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Button driven API calls are a common React pattern for search tools, admin actions, and on demand reports. The key is to keep network state explicit so loading, success, and error behaviors remain predictable. A robust implementation also handles repeated clicks, cancellation, and stale response ordering.

Build a Clear State Model First

When a request starts on click, track at least four pieces of state:

  • current status
  • response data
  • user facing error message
  • last request identifier

You can model this with useState, or with useReducer if the flow grows. For many screens, useState is enough.

jsx
1import { useState } from "react";
2
3export default function UserLookup() {
4  const [status, setStatus] = useState("idle");
5  const [user, setUser] = useState(null);
6  const [error, setError] = useState("");
7
8  async function handleClick() {
9    setStatus("loading");
10    setError("");
11
12    try {
13      const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
14      if (!res.ok) throw new Error(`Request failed with status ${res.status}`);
15      const data = await res.json();
16      setUser(data);
17      setStatus("success");
18    } catch (err) {
19      setUser(null);
20      setError(err instanceof Error ? err.message : "Unknown error");
21      setStatus("error");
22    }
23  }
24
25  return (
26    <section>
27      <button onClick={handleClick} disabled={status === "loading"}>
28        {status === "loading" ? "Loading..." : "Load user"}
29      </button>
30      {status === "error" && <p>{error}</p>}
31      {status === "success" && <pre>{JSON.stringify(user, null, 2)}</pre>}
32    </section>
33  );
34}

This pattern is readable and handles normal failure conditions cleanly.

Prevent Stale Responses During Rapid Clicks

If users can trigger new requests quickly, a slower old response may arrive after a newer request and overwrite correct data. Solve this with cancellation and request identity.

jsx
1import { useRef, useState } from "react";
2
3export default function SearchById() {
4  const [id, setId] = useState("1");
5  const [result, setResult] = useState(null);
6  const [status, setStatus] = useState("idle");
7  const [error, setError] = useState("");
8  const currentController = useRef(null);
9
10  async function handleFetch() {
11    currentController.current?.abort();
12    const controller = new AbortController();
13    currentController.current = controller;
14
15    setStatus("loading");
16    setError("");
17
18    try {
19      const res = await fetch(
20        `https://jsonplaceholder.typicode.com/users/${id}`,
21        { signal: controller.signal }
22      );
23
24      if (!res.ok) throw new Error(`User ${id} not found`);
25      const data = await res.json();
26      setResult(data);
27      setStatus("success");
28    } catch (err) {
29      if (err instanceof DOMException && err.name === "AbortError") {
30        return;
31      }
32      setStatus("error");
33      setError(err instanceof Error ? err.message : "Request failed");
34    }
35  }
36
37  return (
38    <div>
39      <input value={id} onChange={(e) => setId(e.target.value)} />
40      <button onClick={handleFetch} disabled={status === "loading"}>Fetch</button>
41      {status === "error" && <p>{error}</p>}
42      {status === "success" && <p>{result?.name}</p>}
43    </div>
44  );
45}

Cancellation keeps UI aligned with the latest user intent.

Move API Calls Out of the Component

As screens grow, keep component code focused on rendering and state transitions. Put HTTP logic in a small API module.

javascript
1// api/users.js
2export async function fetchUserById(id, signal) {
3  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, { signal });
4  if (!res.ok) {
5    throw new Error(`Fetch failed with status ${res.status}`);
6  }
7  return res.json();
8}

Then import this function inside your component. That makes unit tests easier and reduces duplication across pages.

Decide UI Behavior for Existing Data

One design decision matters: do you clear old data on every click, or keep old data until the new request succeeds.

Good default for dashboards:

  • keep previous data visible
  • show loading state on top
  • replace data only after success

Good default for destructive operations:

  • clear old result immediately
  • display explicit pending state
  • show strong error if request fails

Pick one behavior intentionally so the interface feels consistent.

Testing the Click to Fetch Flow

Use React Testing Library and mock fetch to verify state transitions.

jsx
1import { render, screen } from "@testing-library/react";
2import userEvent from "@testing-library/user-event";
3import UserLookup from "./UserLookup";
4
5test("loads and displays user name", async () => {
6  global.fetch = jest.fn().mockResolvedValue({
7    ok: true,
8    json: async () => ({ id: 1, name: "Leanne Graham" })
9  });
10
11  render(<UserLookup />);
12  await userEvent.click(screen.getByRole("button", { name: /load user/i }));
13
14  expect(await screen.findByText(/Leanne Graham/i)).toBeInTheDocument();
15});

This protects against regressions when refactoring event handlers.

Common Pitfalls

A common mistake is treating only thrown network errors as failures and ignoring non success HTTP status codes. Always check response.ok.

Another issue is allowing overlapping requests without cancellation. Users then see results from older clicks and lose trust in the interface.

Teams also place all fetching logic inline in components, which makes code hard to test and harder to reuse.

Summary

  • Trigger button based fetches through explicit state transitions.
  • Track loading, success, and error states independently.
  • Prevent stale updates with AbortController when clicks can overlap.
  • Extract API logic into separate modules as complexity grows.
  • Test the click flow so refactors do not break user visible behavior.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.