Data Fetching in React

16 Jul 2025

Data Fetching in React cover image.
MS

Salman Alfarisi

Fullstack Engineer

Introduction

Data fetching is a common requirement in modern web applications. In this post, we'll start with a vanilla React approach using useEffect, add basic state handling for loading and error, then switch to React Query and compare the differences.


Basic React Fetch

import { useEffect, useState } from "react";
 
function Users() {
  const [users, setUsers] = useState([]);
 
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((res) => res.json())
      .then((data) => setUsers(data));
  }, []);
 
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

This works, but there's no feedback while loading or on failure.


Adding Logic and States

import { useEffect, useState } from "react";
 
function Users() {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [isError, setIsError] = useState(false);
 
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((res) => {
        if (!res.ok) throw new Error("Failed to fetch");
        return res.json();
      })
      .then((data) => {
        setUsers(data);
        setIsLoading(false);
      })
      .catch(() => {
        setIsError(true);
        setIsLoading(false);
      });
  }, []);
 
  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>Something went wrong.</p>;
 
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

This adds minimal loading and error handling, but logic is verbose and repeated for every fetch.


Introducing React Query

Install React Query:

npm install @tanstack/react-query

Wrap your app with the QueryClientProvider:

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 
import Users from "./Users";
 
const queryClient = new QueryClient();
 
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Users />
    </QueryClientProvider>
  );
}

Fetching with useQuery

import { useQuery } from "@tanstack/react-query";
 
function Users() {
  const { data, isLoading, isError } = useQuery({
    queryKey: ["users"],
    queryFn: () =>
      fetch("https://jsonplaceholder.typicode.com/users").then((res) => {
        if (!res.ok) throw new Error("Network error");
        return res.json();
      }),
  });
 
  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>Error fetching users.</p>;
 
  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

With just a few lines, React Query gives us:

  • isLoading and isError states
  • Caching and background updates
  • Retry and stale data control
  • Devtools for debugging

Comparison Table

FeatureuseEffect + fetchReact Query
Data fetching
Loading state❌ (manual)
Error handling❌ (manual)
Caching
Background refetching
Pagination / Infinite❌ (manual)
Devtools
BoilerplateHighLow

Summary

Using useEffect is fine for small apps, but quickly becomes repetitive and error-prone. React Query simplifies data fetching and improves user experience with built-in caching, background sync, and loading/error handling — all with minimal code.

Explore more at the React Query docs.


  • Image by Mike Yukhtenko