Skip to content
React rc data 4 min read

Fetching with fetch & axios

Most React apps eventually need to talk to a server, and the two workhorses for that are the browser’s built-in Fetch API and the popular axios library. Both let you issue GET, POST, and other HTTP requests, but they differ in ergonomics around JSON parsing, error handling, and configuration. Understanding their raw behavior is essential even if you later adopt a data-fetching library, because those libraries are built on top of exactly these primitives.

Making a GET request with fetch

fetch returns a promise that resolves to a Response object. Crucially, the promise does not reject on HTTP error codes like 404 or 500 — it only rejects on network failures. You must check response.ok (true for 200-299) yourself and call response.json() to parse the body.

import { useEffect, useState } from "react";

export default function UserList() {
  const [users, setUsers] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function load() {
      try {
        const res = await fetch("https://jsonplaceholder.typicode.com/users");
        if (!res.ok) {
          throw new Error(`Request failed: ${res.status}`);
        }
        const data = await res.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      }
    }
    load();
  }, []);

  if (error) return <p>Error: {error}</p>;
  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

Output:

Leanne Graham
Ervin Howell
Clementine Bauch
...

Sending data with a POST request

For a POST you pass a second options argument with the method, a JSON-serialized body, and a Content-Type header so the server knows how to parse the payload.

async function createPost(title, body) {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title, body, userId: 1 }),
  });
  if (!res.ok) throw new Error(`Create failed: ${res.status}`);
  return res.json();
}

const created = await createPost("Hello", "My first post");
console.log(created);

Output:

{ title: "Hello", body: "My first post", userId: 1, id: 101 }

The same requests with axios

axios is a third-party HTTP client (npm install axios). It parses JSON automatically into response.data, throws on non-2xx status codes by default, and supports request/response interceptors and base configuration. The data already lives on response.data — no second await for parsing.

import axios from "axios";

// GET
const { data: users } = await axios.get(
  "https://jsonplaceholder.typicode.com/users"
);

// POST — body is the second argument, serialized for you
const { data: created } = await axios.post(
  "https://jsonplaceholder.typicode.com/posts",
  { title: "Hello", body: "My first post", userId: 1 }
);

fetch vs. axios at a glance

Concernfetchaxios
Built in?Yes (browser & Node 18+)No — external dependency
JSON parsingManual await res.json()Automatic via response.data
HTTP error handlingCheck res.ok yourselfRejects on 4xx/5xx automatically
Request bodyStringify manuallySerialized automatically
TimeoutsVia AbortControllerBuilt-in timeout option
InterceptorsNoYes

Tip: fetch will happily resolve a 404 response. Forgetting the res.ok check is the single most common bug when fetching data — you end up trying to parse an error page as JSON.

Headers and authentication

Both clients let you attach headers such as a bearer token. With fetch you spell out the headers per call; with axios you can set defaults or use an interceptor so every request is authenticated.

// fetch
await fetch("https://api.example.com/me", {
  headers: {
    Authorization: `Bearer ${token}`,
    Accept: "application/json",
  },
});

// axios — apply a token to every outgoing request
axios.interceptors.request.use((config) => {
  config.headers.Authorization = `Bearer ${token}`;
  return config;
});

Wrapping calls in an api module

Scattering raw fetch/axios calls through components couples your UI to transport details. Centralize them in a small api module so the base URL, headers, and error handling live in one place and components just call named functions.

// src/api/client.js
import axios from "axios";

const client = axios.create({
  baseURL: "https://jsonplaceholder.typicode.com",
  timeout: 8000,
  headers: { "Content-Type": "application/json" },
});

export const getUsers = () => client.get("/users").then((r) => r.data);
export const getUser = (id) => client.get(`/users/${id}`).then((r) => r.data);
export const createPost = (payload) =>
  client.post("/posts", payload).then((r) => r.data);
// src/components/UserList.jsx
import { useEffect, useState } from "react";
import { getUsers } from "../api/client";

export default function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    getUsers().then(setUsers).catch(console.error);
  }, []);

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

This keeps components declarative: if the base URL changes or you need to add auth, you edit one file rather than dozens of call sites.

Best Practices

  • Always check response.ok with fetch before parsing — it does not reject on 4xx/5xx.
  • Wrap requests in try/catch (or .catch) so network failures surface as visible UI state.
  • Centralize transport concerns (base URL, headers, timeouts) in an api module instead of inlining them in components.
  • Use an AbortController with fetch to cancel in-flight requests on unmount and avoid state updates on dead components.
  • Prefer axios interceptors for cross-cutting needs like attaching tokens or normalizing errors.
  • Never store secrets in client code; send tokens via headers obtained from a secure flow.
  • For anything beyond simple cases, layer a caching library on top rather than hand-rolling request state.
Last updated June 14, 2026
Was this helpful?