How To Get React to Fetch Data From an API
TL;DR: There are four main ways to fetch data in React: the native Fetch API with useEffect, Axios for more feature-rich HTTP requests, TanStack Query (formerly React Query) for declarative server-state management, and React Server Components for server-side fetching in frameworks like Next.js. Which you reach for depends on your project’s complexity, framework, and whether data lives on the client or server.
Fetching data from an API is one of the most common tasks in React development. React doesn’t prescribe a specific approach — it leaves the choice to you — which means there are several valid patterns, each with different trade-offs. This guide covers all of them with accurate, up-to-date code examples so you can choose the right tool for your use case.
The Four Main Approaches
Before diving in, here’s a quick orientation of when to use each method:
- Fetch API + useEffect — Simple, no dependencies, good for straightforward client-side fetching in smaller apps or isolated components.
- Axios + useEffect — Like Fetch but with a cleaner API, automatic JSON parsing, and better interceptor support. Good for apps already using Axios.
- TanStack Query (useQuery) — The go-to for client-side data in real-world apps. Handles caching, background refetching, deduplication, and loading/error states automatically.
- React Server Components — The modern framework-native approach for Next.js 13+ and other RSC-enabled frameworks. Fetches data on the server with zero client-side JavaScript overhead.
1. The Native Fetch API with useEffect
The Fetch API is built into the browser — no libraries needed. Combined with useEffect and useState, it’s the most lightweight way to fetch data in a React component.
import { useState, useEffect } from 'react';
function UserList() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch('https://api.example.com/users', { signal: controller.signal })
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err.message);
setLoading(false);
}
});
return () => controller.abort();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}
A few things worth noting in this example that are often skipped in basic tutorials:
- AbortController — The cleanup function calls
controller.abort()when the component unmounts, cancelling the in-flight request and preventing state updates on unmounted components (a common source of memory leak warnings). - Response.ok check — Fetch doesn’t throw on HTTP error status codes (like 404 or 500). You need to check
response.okmanually and throw if needed. - Separate loading and error states — Essential for a good user experience.
When to use it: Simple components or small apps where adding a library feels like overkill. If you’re writing the same loading/error boilerplate in five places, it’s time to move to TanStack Query.
2. Axios: A Cleaner HTTP Client
Axios is a popular third-party HTTP library that improves on the native Fetch API in several ways: it throws automatically on non-2xx status codes, parses JSON responses by default, supports request/response interceptors for auth headers and logging, and has cleaner timeout handling.
Install it first:
npm install axios
Then use it with useEffect:
import { useState, useEffect } from 'react';
import axios from 'axios';
function UserList() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
axios.get('https://api.example.com/users', { signal: controller.signal })
.then(response => {
setData(response.data);
setLoading(false);
})
.catch(err => {
if (!axios.isCancel(err)) {
setError(err.message);
setLoading(false);
}
});
return () => controller.abort();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}
Axios also works well with async/await for cleaner sequential logic:
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await axios.get('https://api.example.com/users', {
signal: controller.signal
});
setData(response.data);
} catch (err) {
if (!axios.isCancel(err)) setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, []);
When to use it: Projects that need interceptors (attaching auth tokens to every request, for example), centralized error handling, or that already have Axios configured as a base instance across the codebase.
3. TanStack Query (Formerly React Query)
TanStack Query is the most widely adopted solution for managing server state in React. It treats data fetched from APIs as a distinct concern from UI state — handling caching, background refetching, request deduplication, stale-while-revalidate patterns, and loading/error states automatically, so you don’t have to.
Note: “React Query” was renamed to TanStack Query when it expanded to support other frameworks. The React package is @tanstack/react-query.
Install it:
npm install @tanstack/react-query
Wrap your app in the provider (typically in your root component):
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<UserList />
</QueryClientProvider>
);
}
Then use useQuery in your components — note the current v5 API uses an options object, not a positional string key:
import { useQuery } from '@tanstack/react-query';
async function fetchUsers() {
const response = await fetch('https://api.example.com/users');
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
}
function UserList() {
const { data, error, isPending } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}
What TanStack Query handles for you automatically that plain useEffect doesn’t:
- Caching — The same query across multiple components only fires one network request. Subsequent renders use cached data instantly.
- Background refetching — When a user returns to a tab or reconnects to the network, stale data is quietly refreshed in the background.
- Request deduplication — Multiple components mounting simultaneously won’t fire duplicate requests for the same data.
- Automatic retries — Failed requests are retried automatically (configurable).
- Mutations — The
useMutationhook handles POST/PUT/DELETE operations with optimistic update support and cache invalidation.
When to use it: Any real-world app where data is shared across components, needs to stay fresh, or where writing loading/error boilerplate in every component is becoming a burden. For most production React apps, TanStack Query is the right default for client-side data fetching.
4. React Server Components
React Server Components (RSC) represent a fundamentally different approach to data fetching — instead of fetching data on the client after the component renders, the component fetches its data on the server before sending any HTML to the browser. This eliminates client-side loading spinners for the initial data load, reduces JavaScript bundle size, and avoids request waterfalls on the client.
In Next.js 13+ with the App Router, all components are Server Components by default. Fetching data is as simple as making a component async:
// app/users/page.tsx — Server Component (no 'use client' directive)
async function UserList() {
const response = await fetch('https://api.example.com/users', {
next: { revalidate: 60 } // revalidate every 60 seconds
});
const users = await response.json();
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
There’s no useEffect, no useState, no loading state management — the data is ready before the component renders. Next.js extends fetch with caching options (force-cache, no-store, revalidate) to give you fine-grained control over freshness.
For highly interactive components that still need client-side data (real-time updates, user-triggered refetches), the recommended pattern in modern Next.js apps is to combine Server Components for the initial load with TanStack Query for client-side updates — giving you the best of both.
When to use it: Framework-based React apps using Next.js 13+ App Router (or TanStack Start, Remix). Not available in plain Create React App or Vite setups without a framework layer.
Best Practices for Fetching Data in React
Always handle errors explicitly. Fetch doesn’t throw on 4xx/5xx responses — check response.ok and throw manually. TanStack Query propagates thrown errors through its error state automatically.
Cancel requests on unmount. Use AbortController with a cleanup function in useEffect to cancel in-flight requests when a component unmounts. This prevents “can’t perform a state update on an unmounted component” warnings.
Show meaningful loading states. Avoid blank screens — display skeletons or spinners while data loads. TanStack Query’s isPending and isFetching flags make this easy to distinguish between “no data yet” and “refreshing in the background.”
Use query keys thoughtfully. In TanStack Query, query keys are the cache identity for your data. Include any variables the query depends on — ['users', userId] rather than just ['users'] — so the cache is correctly invalidated when parameters change.
Don’t fetch in useEffect when a framework can do it for you. If you’re using Next.js, Remix, or TanStack Start, reaching for Server Components or loader functions is often cleaner and more performant than client-side useEffect fetching for initial page data.
Cache deliberately. Both TanStack Query’s staleTime/gcTime settings and Next.js’s fetch cache options give you control over how long data stays fresh. Set these based on how frequently your data actually changes — aggressive caching can significantly improve perceived performance.
Choosing the Right Approach
| Scenario | Recommended Approach |
|---|---|
| Simple component, no framework | Fetch API + useEffect + AbortController |
| Need interceptors or centralized HTTP config | Axios + useEffect |
| Data shared across components / needs caching | TanStack Query |
| Mutations with cache invalidation | TanStack Query (useMutation) |
| Next.js App Router / RSC-enabled framework | React Server Components (+ TanStack Query for interactive parts) |
| Real-time data (WebSockets, polling) | TanStack Query with refetchInterval, or dedicated WS library |
Frequently Asked Questions
Is it bad to fetch data in useEffect?
Not inherently — it’s a valid pattern for simple cases. The problem is that raw useEffect data fetching doesn’t handle caching, deduplication, or background refetching, so it tends to accumulate boilerplate as apps grow. For anything beyond a single component fetching its own isolated data, TanStack Query is a better fit.
What’s the difference between React Query and TanStack Query?
They’re the same library. React Query was renamed TanStack Query when the project expanded support beyond React to Vue, Solid, and other frameworks. The React-specific package is @tanstack/react-query and the API is the same.
Should I use the v3 or v5 API for TanStack Query?
Use v5. The old positional string key syntax — useQuery('key', fn) — was deprecated in v4 and removed in v5. The current API uses an options object: useQuery({ queryKey: ['key'], queryFn: fn }). Also note that isLoading was renamed to isPending in v5.
Do React Server Components replace TanStack Query?
For initial data loads in framework-based apps, yes — Server Components are often cleaner. But TanStack Query still handles use cases that RSC doesn’t: client-side mutations, optimistic updates, polling, infinite scroll, and any data that needs to stay live after the initial page load. The hybrid pattern (RSC for initial render, TanStack Query for interactivity) has become the dominant approach in Next.js apps.
How do I handle authentication headers when fetching data?
With Axios, set up a base instance with default headers. With Fetch or TanStack Query, create a wrapper function that adds your auth header before calling fetch. TanStack Query doesn’t manage HTTP configuration directly — it calls whatever queryFn you provide, so auth handling lives in that function.
For a deeper look at how React handles state alongside data, see our guide on React Hooks basics including useState and useEffect. If you’re using Next.js as your React framework, our Next.js basics guide covers the server-rendering fundamentals that underpin React Server Components.
