React Fetch Data from API

Fetching data from an API in React is one of the most common tasks you will do. The basic pattern combines useState for the data, loading and error states with useEffect to start the request when the component appears.

Fetching Data with useEffect

The async function lives inside the effect. fetch does not reject on HTTP errors like 404, so we check res.ok ourselves.

import { useState, useEffect } from 'react';

function Users() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  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('HTTP ' + res.status);
        setUsers(await res.json());
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }
    load();
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
Loading...
• Leanne Graham
• Ervin Howell
• Clementine Bauch
...

Fetching When a Value Changes

Put the value in the dependency array to refetch whenever it changes, for example a user id or a search term. Use AbortController in the cleanup so an old, slow response cannot overwrite a newer one.

function Post({ id }) {
  const [post, setPost] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, { signal: controller.signal })
      .then((res) => res.json())
      .then(setPost)
      .catch((err) => {
        if (err.name !== 'AbortError') console.error(err);
      });
    return () => controller.abort();
  }, [id]);

  return post ? <h2>{post.title}</h2> : <p>Loading...</p>;
}
sunt aut facere repellat provident occaecati excepturi optio reprehenderit

Sending Data with POST

Writes usually happen in event handlers, not effects, because they are triggered by the user.

async function addTodo(title) {
  const res = await fetch('/api/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title }),
  });
  const saved = await res.json();
  setTodos((prev) => [...prev, saved]);
}
(new todo appended to the list)

Data Fetching Tips

  • Always handle three states: loading, error and success.
  • Keep API URLs in environment variables: import.meta.env.VITE_API_URL in Vite.
  • For caching, retries and background refresh, consider TanStack Query or SWR.
  • Frameworks like Next.js can fetch data on the server before the page loads.

Frequently asked questions

Should I use fetch or axios in React?

fetch is built into browsers and is enough for most apps. Axios adds conveniences like automatic JSON parsing and interceptors. Both work the same way inside useEffect.

Why does my React fetch run twice?

In development, Strict Mode runs effects twice to expose missing cleanups. Use an AbortController cleanup; in production the effect runs once.

How do I show a loading spinner while fetching in React?

Keep a loading state that starts as true, set it to false when the request finishes, and render a spinner while it is true.