Fetch API & Axios

Fetching Data in React

React does not have a built-in data fetching solution. The two most common approaches are the native fetch API and the Axios library. Both are typically used inside useEffect or a custom hook.

fetch API

// GET request
useEffect(() => {
  fetch("https://api.example.com/posts")
    .then(res => {
      if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
      return res.json();
    })
    .then(data => setPosts(data))
    .catch(err => setError(err.message));
}, []);

// POST request with JSON body
const createPost = async (newPost) => {
  const res = await fetch("https://api.example.com/posts", {
    method:  "POST",
    headers: { "Content-Type": "application/json" },
    body:    JSON.stringify(newPost),
  });
  if (!res.ok) throw new Error("Failed to create post");
  return res.json();
};

Axios

npm install axios
import axios from "axios";

// Create an instance with base URL and default headers
const api = axios.create({
  baseURL: "https://api.example.com",
  headers: { "Content-Type": "application/json" },
  timeout: 10000,
});

// Add auth token to every request
api.interceptors.request.use(config => {
  const token = localStorage.getItem("token");
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// GET
const { data: posts } = await api.get("/posts");

// POST
const { data: newPost } = await api.post("/posts", { title, body, userId });

// PUT
const { data: updated } = await api.put(`/posts/${id}`, payload);

// DELETE
await api.delete(`/posts/${id}`);

For production apps, TanStack Query is the gold standard. It handles caching, loading states, error states, background refetching, and optimistic updates automatically.

npm install @tanstack/react-query
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

// Fetch with automatic caching and loading state
function PostList() {
  const { data: posts, isPending, isError } = useQuery({
    queryKey:  ["posts"],
    queryFn:   () => api.get("/posts").then(r => r.data),
    staleTime: 5 * 60 * 1000, // cache for 5 minutes
  });

  if (isPending) return <Spinner />;
  if (isError)   return <p>Failed to load posts.</p>;
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}

// Mutation with cache invalidation
function CreatePostForm() {
  const qc = useQueryClient();
  const mutation = useMutation({
    mutationFn: (data) => api.post("/posts", data).then(r => r.data),
    onSuccess:  () => qc.invalidateQueries({ queryKey: ["posts"] }),
  });

  return <button onClick={() => mutation.mutate({ title: "New Post" })}>Create</button>;
}
NoteTanStack Query replaces the need for manual loading/error state in most components. It’s the recommended approach for any app that talks to an API.