How to Fetch Api in React

How to Fetch API in React Modern web applications rely heavily on external data sources to deliver dynamic, interactive experiences. Whether you're pulling user profiles from a backend service, displaying real-time stock prices, or loading product catalogs from a headless CMS, fetching data from APIs is a fundamental skill for any React developer. In React, fetching an API means retrieving data fr

Oct 30, 2025 - 13:15
Oct 30, 2025 - 13:15
 0

How to Fetch API in React

Modern web applications rely heavily on external data sources to deliver dynamic, interactive experiences. Whether you're pulling user profiles from a backend service, displaying real-time stock prices, or loading product catalogs from a headless CMS, fetching data from APIs is a fundamental skill for any React developer. In React, fetching an API means retrieving data from an external endpoint and rendering it within your components UI. This process is essential for building responsive, data-driven applications that go beyond static content.

React itself does not include built-in methods for making HTTP requests, but it provides the tools and lifecycle hooks necessary to integrate with JavaScripts native Fetch API or third-party libraries like Axios. Understanding how to fetch APIs in React is not just about writing a line of codeits about mastering asynchronous data flow, managing loading states, handling errors gracefully, and optimizing performance for the best user experience.

This tutorial will guide you through every critical aspect of fetching APIs in Reactfrom the foundational steps to advanced best practices. By the end, youll be equipped to confidently integrate external data sources into your React applications, avoid common pitfalls, and write clean, scalable, and maintainable code.

Step-by-Step Guide

Understanding the Fetch API in JavaScript

Before diving into React-specific implementations, its essential to understand the native JavaScript Fetch API. The Fetch API is a modern, promise-based interface for making HTTP requests. Unlike the older XMLHttpRequest, Fetch is more powerful, flexible, and easier to use with async/await syntax.

Heres a basic example of how to fetch data using the native Fetch API:

fetch('https://jsonplaceholder.typicode.com/posts/1')

.then(response => response.json())

.then(data => console.log(data))

.catch(error => console.error('Error:', error));

In this example:

  • fetch() initiates the HTTP request to the specified URL.
  • .then(response => response.json()) converts the response into JSON format.
  • .then(data => console.log(data)) handles the parsed data.
  • .catch() catches any network or HTTP errors.

While this works in plain JavaScript, integrating it into React requires careful management of state, side effects, and component lifecycle eventsespecially since React components re-render frequently.

Setting Up a React Project

If you havent already created a React project, start by using Create React App (CRA) or Vite. For this tutorial, well use CRA:

npx create-react-app api-fetch-demo

cd api-fetch-demo

npm start

This sets up a basic React application with Webpack, Babel, and all necessary tooling. Youll be working inside the src folder, primarily editing App.js and creating new components as needed.

Using useEffect to Fetch Data on Component Mount

Reacts useEffect hook is the standard way to perform side effectslike data fetchingin functional components. Side effects are operations that affect something outside the components scope, such as fetching data, subscribing to events, or manipulating the DOM.

To fetch data when a component mounts, pass an empty dependency array ([]) to useEffect. This ensures the effect runs only once after the initial render.

Heres a complete example:

import React, { useState, useEffect } from 'react';

function App() {

const [data, setData] = useState(null);

const [loading, setLoading] = useState(true);

const [error, setError] = useState(null);

useEffect(() => {

fetch('https://jsonplaceholder.typicode.com/posts/1')

.then(response => {

if (!response.ok) {

throw new Error('Network response was not ok');

}

return response.json();

})

.then(data => {

setData(data);

setLoading(false);

})

.catch(error => {

setError(error.message);

setLoading(false);

});

}, []); // Empty dependency array ensures this runs only once

if (loading) return <p>Loading...</p>;

if (error) return <p>Error: {error}</p>;

return (

<div>

<h2>{data.title}</h2>

<p>{data.body}</p>

</div>

);

}

export default App;

In this example:

  • useState is used to manage three pieces of state: data (the fetched data), loading (to show a spinner or placeholder), and error (to display user-friendly error messages).
  • useEffect runs the fetch request when the component mounts.
  • The component renders different UIs based on state: loading, error, or success.

This pattern is the foundation of most data-fetching logic in React applications.

Fetching Data with Async/Await

While the .then() chain works, many developers prefer the cleaner, more readable syntax of async/await. Heres the same example rewritten using async/await:

import React, { useState, useEffect } from 'react';

function App() {

const [data, setData] = useState(null);

const [loading, setLoading] = useState(true);

const [error, setError] = useState(null);

useEffect(() => {

const fetchData = async () => {

try {

const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');

if (!response.ok) {

throw new Error('Network response was not ok');

}

const result = await response.json();

setData(result);

} catch (err) {

setError(err.message);

} finally {

setLoading(false);

}

};

fetchData();

}, []);

if (loading) return <p>Loading...</p>;

if (error) return <p>Error: {error}</p>;

return (

<div>

<h2>{data?.title}</h2>

<p>{data?.body}</p>

</div>

);

}

export default App;

Key improvements:

  • async and await make the code look synchronous, improving readability.
  • The finally block ensures loading is set to false regardless of success or failure.
  • Optional chaining (data?.title) prevents errors if data is null or undefined during initial render.

Fetching Multiple Resources

Often, youll need to fetch data from multiple endpoints. For example, fetching a user profile and their posts. You can use Promise.all() to run multiple fetch requests in parallel:

useEffect(() => {

const fetchData = async () => {

try {

const [userResponse, postsResponse] = await Promise.all([

fetch('https://jsonplaceholder.typicode.com/users/1'),

fetch('https://jsonplaceholder.typicode.com/posts?userId=1')

]);

if (!userResponse.ok || !postsResponse.ok) {

throw new Error('One or more requests failed');

}

const user = await userResponse.json();

const posts = await postsResponse.json();

setUser(user);

setPosts(posts);

} catch (err) {

setError(err.message);

} finally {

setLoading(false);

}

};

fetchData();

}, []);

Promise.all() waits for all promises to resolve. If any one fails, the entire block enters the catch clause. This is efficient but brittleif one request fails, you lose all data. For more resilient applications, consider using Promise.allSettled(), which resolves regardless of individual outcomes:

const results = await Promise.allSettled([

fetch('https://jsonplaceholder.typicode.com/users/1'),

fetch('https://jsonplaceholder.typicode.com/posts?userId=1')

]);

const [userResult, postsResult] = results;

if (userResult.status === 'fulfilled') {

setUser(await userResult.value.json());

}

if (postsResult.status === 'fulfilled') {

setPosts(await postsResult.value.json());

}

Fetching Data Based on User Input or Props

Often, API calls depend on user interactionlike searching for a product or filtering results. In such cases, youll need to trigger the fetch based on changing state or props.

For example, lets build a search component that fetches GitHub users as the user types:

import React, { useState, useEffect } from 'react';

function SearchUsers() {

const [query, setQuery] = useState('');

const [users, setUsers] = useState([]);

const [loading, setLoading] = useState(false);

const [error, setError] = useState(null);

useEffect(() => {

const fetchUsers = async () => {

if (!query.trim()) {

setUsers([]);

return;

}

setLoading(true);

setError(null);

try {

const response = await fetch(https://api.github.com/search/users?q=${query});

if (!response.ok) {

throw new Error('Failed to fetch users');

}

const data = await response.json();

setUsers(data.items);

} catch (err) {

setError(err.message);

} finally {

setLoading(false);

}

};

fetchUsers();

}, [query]); // Re-run effect whenever query changes

return (

<div>

<input

type="text"

value={query}

onChange={(e) => setQuery(e.target.value)}

placeholder="Search GitHub users..."

/>

{loading && <p>Searching...</p>}

{error && <p>Error: {error}</p>}

<ul>

{users.map(user => (

<li key={user.id}>

<a href={user.html_url} target="_blank" rel="noopener noreferrer">

{user.login}

</a>

</li>

))}

</ul>

</div>

);

}

export default SearchUsers;

Notice that [query] is included in the dependency array. This means the effect runs every time query changesperfect for real-time search. However, this can lead to excessive API calls. To optimize, implement debouncing (discussed in Best Practices).

Handling Authentication and Headers

Many APIs require authentication headerssuch as API keys, OAuth tokens, or JWTs. You can pass headers to the fetch request using the second parameter:

const response = await fetch('https://api.example.com/data', {

method: 'GET',

headers: {

'Authorization': 'Bearer YOUR_ACCESS_TOKEN',

'Content-Type': 'application/json',

},

});

For applications that require consistent headers across multiple requests, create a custom fetch wrapper:

const apiClient = (url, options = {}) => {

const defaultOptions = {

headers: {

'Authorization': Bearer ${localStorage.getItem('token')},

'Content-Type': 'application/json',

},

};

return fetch(url, { ...defaultOptions, ...options })

.then(response => {

if (!response.ok) {

throw new Error(HTTP error! status: ${response.status});

}

return response.json();

});

};

// Usage

const data = await apiClient('https://api.example.com/profile');

This approach centralizes authentication logic and reduces code duplication.

Best Practices

Always Handle Loading and Error States

Never assume an API request will succeed. Users need feedback during loading and clear guidance when something goes wrong. Always render:

  • A loading indicator (spinner, skeleton screen, progress bar)
  • An error message thats user-friendly (not raw stack traces)
  • A retry mechanism if appropriate

Example of a retry button:

{error && (

<div>

<p>Failed to load data. Please try again.</p>

<button onClick={fetchData}>Retry</button>

</div>

)}

Use Debouncing for Search Queries

Fetching data on every keystroke can overwhelm your backend and degrade performance. Implement debouncing to delay the API call until the user pauses typing:

import { useState, useEffect } from 'react';

function useDebounce(value, delay) {

const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {

const handler = setTimeout(() => {

setDebouncedValue(value);

}, delay);

return () => {

clearTimeout(handler);

};

}, [value, delay]);

return debouncedValue;

}

// In component:

const [query, setQuery] = useState('');

const debouncedQuery = useDebounce(query, 500); // Wait 500ms after typing stops

useEffect(() => {

if (debouncedQuery) {

fetchUsers(debouncedQuery);

}

}, [debouncedQuery]);

Avoid Memory Leaks with AbortController

If a component unmounts before a fetch completes, you risk setting state on an unmounted component, which triggers a warning in development and can cause memory leaks. Use AbortController to cancel requests:

useEffect(() => {

const controller = new AbortController();

const fetchData = async () => {

try {

const response = await fetch('https://api.example.com/data', {

signal: controller.signal,

});

const data = await response.json();

setData(data);

} catch (err) {

if (err.name === 'AbortError') {

console.log('Fetch aborted');

return;

}

setError(err.message);

}

};

fetchData();

return () => controller.abort(); // Cancel request on unmount

}, []);

This ensures no stale state updates occur after the component is removed from the DOM.

Normalize Data Structure and Avoid Nested State

When fetching complex data (e.g., users with posts, comments, and likes), avoid deeply nested state objects. Instead, normalize your state using libraries like Redux Toolkit or simply flatten the structure:

// Instead of:

state = {

user: {

id: 1,

name: 'John',

posts: [

{ id: 101, title: 'Post 1', comments: [...] }

]

}

}

// Use:

state = {

users: { 1: { name: 'John' } },

posts: { 101: { title: 'Post 1', userId: 1 } },

comments: { 201: { postId: 101, text: 'Great!' } }

}

This improves performance, simplifies updates, and makes caching easier.

Cache Responses to Reduce Redundant Requests

Repeatedly fetching the same data wastes bandwidth and increases latency. Implement client-side caching using:
  • Local storage for persistent caching
  • Memory cache (JavaScript Map object) for short-term caching
const cache = new Map();

const fetchWithCache = async (url) => {

if (cache.has(url)) {

console.log('Serving from cache');

return cache.get(url);

}

const response = await fetch(url);

const data = await response.json();

cache.set(url, data); // Cache for the lifetime of the app

return data;

};

For production apps, consider using libraries like React Query or SWR, which provide advanced caching, stale-while-revalidate, and background refetching out of the box.

Separate Data Fetching Logic into Custom Hooks

Reusability is key in React. Extract data-fetching logic into custom hooks to avoid duplication:

function useApi(url) {

const [data, setData] = useState(null);

const [loading, setLoading] = useState(true);

const [error, setError] = useState(null);

useEffect(() => {

const fetchData = async () => {

try {

const response = await fetch(url);

if (!response.ok) throw new Error('Network error');

const result = await response.json();

setData(result);

} catch (err) {

setError(err.message);

} finally {

setLoading(false);

}

};

fetchData();

}, [url]);

return { data, loading, error };

}

// Usage in component:

function UserProfile({ userId }) {

const { data, loading, error } = useApi(https://api.example.com/users/${userId});

if (loading) return <p>Loading...</p>;

if (error) return <p>Error: {error}</p>;

return <div>{data?.name}</div>;

}

This pattern promotes clean, testable, and maintainable code.

Tools and Resources

React Query (TanStack Query)

React Query is the industry-standard library for managing server state in React. It handles data fetching, caching, background updates, pagination, and mutations with minimal configuration.

Install it:

npm install @tanstack/react-query

Example usage:

import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {

const { data, isLoading, error } = useQuery({

queryKey: ['user', userId],

queryFn: () => fetch(/api/users/${userId}).then(res => res.json()),

});

if (isLoading) return <p>Loading...</p>;

if (error) return <p>Error: {error.message}</p>;

return <div>{data.name}</div>;

}

Benefits:

  • Automatic caching and stale data management
  • Background refetching on window focus
  • Query deduplication
  • Support for pagination, infinite scroll, and mutations

SWR (Stale-While-Revalidate)

Developed by Vercel, SWR is another popular data-fetching library inspired by React Query. Its lightweight and ideal for simple to moderately complex apps.

Install:

npm install swr

Usage:

import useSWR from 'swr';

const fetcher = (...args) => fetch(...args).then(res => res.json());

function UserProfile({ userId }) {

const { data, error } = useSWR(/api/users/${userId}, fetcher);

if (error) return <p>Failed to load</p>;

if (!data) return <p>Loading...</p>;

return <div>{data.name}</div>;

}

SWR automatically revalidates data when the component remounts or the window regains focus.

Axios

While the Fetch API is native, Axios remains a popular alternative due to its robust feature set:

  • Automatic JSON transformation
  • Request and response interceptors
  • Cancelation support
  • Client-side protection against XSRF

Install:

npm install axios

Usage:

import axios from 'axios';

const { data, loading, error } = useAsyncEffect(async () => {

try {

const response = await axios.get('https://api.example.com/data');

return response.data;

} catch (err) {

throw new Error(err.response?.data?.message || err.message);

}

}, []);

Axios is especially useful when working with legacy systems or when you need advanced request/response handling.

Postman and Insomnia

Before integrating an API into React, test it with tools like Postman or Insomnia. These tools let you:

  • Inspect request/response headers
  • Test authentication flows
  • Validate JSON structure
  • Generate code snippets for Fetch, Axios, or cURL

JSONPlaceholder and Reqres.in

For development and learning, use mock APIs:

  • JSONPlaceholder: https://jsonplaceholder.typicode.com fake REST API for testing
  • Reqres.in: https://reqres.in lightweight API for user data, pagination, and delays

Browser DevTools

Use the Network tab in Chrome DevTools or Firefox Developer Tools to:

  • Monitor outgoing requests
  • Check response status codes
  • Inspect headers and payloads
  • Simulate slow networks

This is critical for debugging failed requests and optimizing load times.

Real Examples

Example 1: E-Commerce Product List

Imagine a product listing page that fetches items from a backend API. The component supports filtering by category and pagination.

import React, { useState, useEffect } from 'react';

function ProductList() {

const [products, setProducts] = useState([]);

const [category, setCategory] = useState('');

const [page, setPage] = useState(1);

const [loading, setLoading] = useState(true);

const [error, setError] = useState(null);

useEffect(() => {

const fetchProducts = async () => {

const url = new URL('https://api.example.com/products');

url.searchParams.set('category', category);

url.searchParams.set('page', page);

url.searchParams.set('limit', '10');

try {

const response = await fetch(url);

if (!response.ok) throw new Error('Failed to fetch products');

const data = await response.json();

setProducts(data.products);

} catch (err) {

setError(err.message);

} finally {

setLoading(false);

}

};

fetchProducts();

}, [category, page]);

const handleCategoryChange = (e) => {

setCategory(e.target.value);

setPage(1); // Reset to first page on category change

};

if (loading) return <p>Loading products...</p>;

if (error) return <p>Error: {error}</p>;

return (

<div>

<select value={category} onChange={handleCategoryChange}>

<option value="">All Categories</option>

<option value="electronics">Electronics</option>

<option value="clothing">Clothing</option>

</select>

<ul>

{products.map(product => (

<li key={product.id}>

<h3>{product.name}</h3>

<p>${product.price}</p>

</li>

))}

</ul>

<button onClick={() => setPage(p => p + 1)} disabled={loading}>

Load More

</button>

</div>

);

}

export default ProductList;

Example 2: Weather Dashboard with Real-Time Updates

This example fetches weather data every 5 minutes using setInterval and displays current conditions.

import React, { useState, useEffect } from 'react';

function WeatherDashboard() {

const [weather, setWeather] = useState(null);

const [loading, setLoading] = useState(true);

const [error, setError] = useState(null);

useEffect(() => {

const fetchWeather = async () => {

try {

const response = await fetch('https://api.openweathermap.org/data/2.5/weather?q=London&appid=YOUR_API_KEY');

if (!response.ok) throw new Error('Failed to fetch weather');

const data = await response.json();

setWeather(data);

} catch (err) {

setError(err.message);

} finally {

setLoading(false);

}

};

fetchWeather();

// Refresh every 5 minutes

const interval = setInterval(fetchWeather, 5 * 60 * 1000);

return () => clearInterval(interval); // Cleanup on unmount

}, []);

if (loading) return <p>Loading weather data...</p>;

if (error) return <p>Error: {error}</p>;

return (

<div>

<h2>{weather.name}</h2>

<p>Temperature: {Math.round(weather.main.temp - 273.15)}C</p>

<p>Condition: {weather.weather[0].description}</p>

</div>

);

}

export default WeatherDashboard;

Example 3: Form Submission with API Call

When a user submits a form, you often need to POST data to an API and handle success/error states.

import React, { useState } from 'react';

function ContactForm() {

const [formData, setFormData] = useState({ name: '', email: '', message: '' });

const [submitting, setSubmitting] = useState(false);

const [success, setSuccess] = useState(false);

const [error, setError] = useState(null);

const handleChange = (e) => {

setFormData({ ...formData, [e.target.name]: e.target.value });

};

const handleSubmit = async (e) => {

e.preventDefault();

setSubmitting(true);

setError(null);

setSuccess(false);

try {

const response = await fetch('/api/contact', {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: JSON.stringify(formData),

});

if (!response.ok) throw new Error('Submission failed');

setSuccess(true);

setFormData({ name: '', email: '', message: '' }); // Reset form

} catch (err) {

setError(err.message);

} finally {

setSubmitting(false);

}

};

if (success) return <p>Thank you! Your message has been sent.</p>;

return (

<form onSubmit={handleSubmit}>

<input

name="name"

value={formData.name}

onChange={handleChange}

placeholder="Your name"

required

/>

<input

name="email"

type="email"

value={formData.email}

onChange={handleChange}

placeholder="Your email"

required

/>

<textarea

name="message"

value={formData.message}

onChange={handleChange}

placeholder="Your message"

required

/>

<button type="submit" disabled={submitting}>

{submitting ? 'Sending...' : 'Send'}

</button>

{error && <p style={{ color: 'red' }}>{error}</p>}

</form>

);

}

export default ContactForm;

FAQs

What is the difference between fetch and Axios?

Fetch is a native browser API, lightweight, and promise-based. It requires manual handling of JSON parsing and error status codes. Axios is a third-party library that automatically transforms responses, supports interceptors, and provides better error handling out of the box. Axios is more feature-rich, while fetch is simpler and doesnt require an external dependency.

Can I use async/await with useEffect?

Yes, but you cannot make the useEffect function itself async. Instead, define an async function inside the effect and call it immediately. This avoids syntax errors and ensures proper cleanup.

Why is my API call firing multiple times?

This usually happens if you forget to include a dependency array in useEffect or if you're re-rendering the component too frequently. Always pass an empty array [] for one-time fetches. If you depend on props or state, include them in the array. Also, check for strict mode in developmentit intentionally double-invokes effects to help detect side effects.

How do I handle CORS errors?

CORS (Cross-Origin Resource Sharing) errors occur when your frontend domain doesnt match the APIs allowed origins. This is a server-side issue. You cannot fix it from React. The API server must include appropriate headers like Access-Control-Allow-Origin. For development, use a proxy in your package.json or a tool like Vites proxy feature.

Should I use Redux for API data?

Redux is powerful for global state management but often overkill for API data. Libraries like React Query or SWR are better suited because they handle caching, deduplication, and refetching automatically. Use Redux only if you have complex business logic that requires centralized state management beyond data fetching.

How do I test API calls in React?

Use Jest with React Testing Library. Mock the fetch function using jest.mock or msw (Mock Service Worker) to simulate API responses without hitting real endpoints. This ensures your tests are fast and reliable.

Conclusion

Fetched API data is the lifeblood of modern React applications. From simple static pages to complex dashboards and real-time platforms, the ability to retrieve, manage, and display external data efficiently separates good developers from great ones.

In this guide, weve covered everything from the fundamentals of the Fetch API to advanced patterns like debouncing, caching, and custom hooks. Weve explored real-world examples, industry-leading tools like React Query and SWR, and best practices that ensure your applications are fast, reliable, and maintainable.

Remember: data fetching is not just about making HTTP callsits about managing state, handling user expectations, and optimizing performance. Always prioritize user experience by showing loading states, handling errors gracefully, and minimizing unnecessary requests.

As you continue building React applications, dont hesitate to experiment with different libraries and patterns. Start with native fetch to understand the basics, then adopt React Query or SWR for production apps. With the right approach, fetching APIs in React becomes not just a technical task, but a seamless part of the user journey.