Skip to content

React Tutorial Data and APIs

Using the Fetch API in React: GET and POST

Lesson 91 of 156 2 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson · press Try it on the examples to edit and run them here.

fetch is built into every modern browser, so you can call APIs from React with no library. This lesson shows the two shapes you will write most: GET to load data and POST to send it. (Revisit The Fetch API in the JavaScript course for the basics.)

In the playground, requests to other websites are blocked, so the examples use fetch against data: URLs and a small simulated server. The code is the same as with a real API, apart from the URL.

GET: load data when the component appears

App.jsx
JSX
// A "server response" as a data URL
const url = "data:application/json," + encodeURIComponent(JSON.stringify([
  { id: 1, name: "Ada Lovelace" },
  { id: 2, name: "Grace Hopper" },
]));

function App() {
  const [users, setUsers] = React.useState(null);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    let ignore = false;

    async function load() {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error("Request failed: " + response.status);   // fetch does not throw on 404 or 500
        }
        const data = await response.json();
        if (!ignore) setUsers(data);
      } catch (err) {
        if (!ignore) setError(err.message);
      }
    }

    load();
    return () => { ignore = true; };
  }, []);

  if (error) return <p role="alert">Error: {error}</p>;
  if (!users) return <p>Loading</p>;

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

Things to copy into your own code:

  • An async function inside the effect, because the effect function itself cannot be async.
  • if (!response.ok): fetch only rejects on network failure. A 404 or 500 still resolves, so you must check the status.
  • ignore flag in cleanup to avoid updating state after the component is gone or a newer request started. See Fetching Data with Effects.
  • try/catch for both network errors and your own thrown errors.

With a real API, only the URL and options change:

script.js
JS
const response = await fetch("https://api.example.com/users");

POST: send data from an event handler

Requests that change data belong in event handlers (a click or a submit), not in effects. Set the method, a JSON content type and a JSON body:

App.jsx
JSX
async function createUser(user) {
  const response = await fetch("https://api.example.com/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(user),
  });
  if (!response.ok) throw new Error("Could not create user: " + response.status);
  return response.json();
}

Here is the shape with a simulated server, in a complete form:

App.jsx
JSX
// Simulated server: stores users in memory and answers after a short delay
const db = [{ id: 1, name: "Ada" }];
function fakePost(user) {
  return new Promise((resolve) =>
    setTimeout(() => {
      const saved = { id: db.length + 1, ...user };
      db.push(saved);
      resolve(saved);
    }, 500)
  );
}

function App() {
  const [users, setUsers] = React.useState(db);
  const [name, setName] = React.useState("");
  const [saving, setSaving] = React.useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    if (!name.trim()) return;
    setSaving(true);
    try {
      const saved = await fakePost({ name: name.trim() });
      setUsers((list) => [...list, saved]);   // update the screen with the server's answer
      setName("");
    } finally {
      setSaving(false);                       // always leave the busy state
    }
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} />
        <button type="submit" disabled={saving}>{saving ? "Adding..." : "Add user"}</button>
      </form>
      <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
    </div>
  );
}

Keep requests in their own functions

Move fetch calls into plain functions, outside components. They are easier to test, reuse and change (for example when you add an auth header):

script.js
JS
// api.js
const BASE = import.meta.env.VITE_API_URL;

export async function getJson(path, options) {
  const response = await fetch(BASE + path, options);
  if (!response.ok) {
    throw new Error(`Request to ${path} failed with ${response.status}`);
  }
  return response.json();
}

Components then call getJson("/users"). The environment variable is explained in Environment Variables.

What fetch does not do for you

  • Timeouts. A request can hang; use AbortSignal.timeout(ms) or an AbortController; see Aborting Requests.
  • Caching, retries and deduplication. Data libraries provide them; see Data-Fetching Libraries.
  • Sending cookies to other origins. That needs credentials: "include" and matching CORS headers on the server.

Key points

  • Load data in an effect with an async inner function; send data from event handlers.
  • Check response.ok; fetch does not reject on HTTP errors.
  • Put request code in plain functions, and handle failures.