React Data Fetching From an API
Overview
Data fetching is a fundamental part of modern web applications. We use it when we need to log in, find data from an API or even update or delete data. This guide covers different approaches to fetching data in React applications.
This guide covers:
- Different methods of fetching data (
fetch,async/await,axios) - Best practices for handling loading and error states
- Managing API calls triggered by user interactions
- Popular data-fetching libraries
Data Fetching Flow
Here’s a visual representation of the basic data fetching flow in React. We will use this flow to understand the different approaches to fetching data.

Methods of Data Fetching
There are multiple ways to fetch data in React. Let’s look at the most common ones:
1. Using fetch with useEffect
The fetch API is built into modern browsers and provides a simple way to make HTTP requests. It returns a promise that resolves to a Response object. This is a low-level API and requires manual parsing of the response body with .json(), but it is an effective way to fetch data.
function UserList() {
const [users, setUsers] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://api.example.com/users')
// fetch call returns a promise, so we need to chain .then() to handle the response
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
// Response is a promise, so we need to chain .then() to handle the response
.then(data => {
// data is the JSON body of the response
setUsers(data);
setLoading(false);
})
.catch(error => {
// error is an object that contains the error message
setError(error.message);
setLoading(false);
});
}, []); // Empty dependency array means fetch only runs once on mount
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
2. Using async/await with useEffect
async/await provides a more readable way to handle promises by allowing us to write asynchronous code that looks synchronous. This approach is particularly useful for complex data fetching scenarios where you need to handle multiple steps or dependencies.
Key benefits of using async/await:
- More readable and maintainable code
- Simpler error handling with try/catch blocks
- Easier to debug with step-by-step execution
- Simpler to handle multiple sequential async operations
function UserProfile({ userId }) {
// Initialize state for user data, loading status, and potential errors
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// We need to create a separate async function because useEffect's callback
// cannot be directly async (React needs synchronous cleanup)
const fetchUser = async () => {
try {
// Set loading state before starting the fetch
setLoading(true);
// Await the fetch call - this pauses execution until the request completes
const response = await fetch(`https://api.example.com/users/${userId}`);
// Check if the response is ok (status in the range 200-299)
if (!response.ok) {
throw new Error('Failed to fetch user');
}
// Await the JSON parsing - this also returns a promise
const data = await response.json();
// Update the user state with the fetched data
setUser(data);
} catch (err) {
// If any error occurs in the try block, it will be caught here
setError(err.message);
} finally {
// This block always runs, regardless of success or failure
setLoading(false);
}
};
// Call the async function immediately
fetchUser();
// The dependency array includes userId so the effect runs when userId changes
}, [userId]);
// Show appropriate UI based on the current state
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
🎥 Video: Javascript Promiese vs Async Await EXPLAINED (in 5 minutes)
3. Using axios with useEffect
Axios is a popular HTTP client that offers several advantages over the native fetch API. It provides a more powerful and flexible way to make HTTP requests with features like automatic JSON transformation, request/response interceptors, and better error handling.
Key features of axios:
- Automatic JSON data transformation
- Built-in XSRF protection
- Request and response interceptors
- Request cancellation
- Better error handling with detailed error objects
import axios from 'axios';
function PostList() {
// Initialize state for posts, loading status, and errors
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Create an axios instance with default configuration
// This is useful when you need to make multiple requests to the same API
const api = axios.create({
baseURL: 'https://api.example.com', // Base URL for all requests
timeout: 5000, // Request timeout in milliseconds
headers: {
'Content-Type': 'application/json'
// You can add authentication headers here
// 'Authorization': `Bearer ${token}`
}
});
const fetchPosts = async () => {
try {
setLoading(true);
// Axios automatically transforms JSON response
// No need to call .json() like with fetch
const response = await api.get('/posts');
// Axios puts the response body in the .data property
setPosts(response.data);
} catch (err) {
// Axios provides detailed error information
// err.response contains the response from the server
// err.request contains the request that was made
setError(err.response?.data?.message || 'Failed to fetch posts');
} finally {
setLoading(false);
}
};
fetchPosts();
// Optional: Setup request interceptor for authentication
api.interceptors.request.use((config) => {
// You can modify the request config here
// For example, add authentication tokens
return config;
});
// Optional: Setup response interceptor for error handling
api.interceptors.response.use(
response => response,
error => {
// Handle common errors here
if (error.response?.status === 401) {
// Handle unauthorized access
}
return Promise.reject(error);
}
);
}, []); // Empty dependency array means this effect runs once on mount
if (loading) return <div>Loading posts...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
);
}
🎥 Video: Fetch Data with Axios
Triggering API Calls on User Interaction
Sometimes you want to fetch data in response to user actions rather than automatically when a component mounts. This pattern is common in search interfaces, forms, and any interactive features that require fresh data.
Key considerations for user-triggered API calls:
- Debouncing/throttling to prevent too many requests
- Proper loading states for user feedback
- Error handling and retry mechanisms
- Input validation before making requests
function SearchUsers() {
// State for the search query and results
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Handler for the search action
const handleSearch = async () => {
// Validate input before making the request
if (!query.trim()) return;
setLoading(true);
setError(null); // Clear any previous errors
try {
// Encode the query parameter to handle special characters
const response = await fetch(
`https://api.example.com/search?q=${encodeURIComponent(query)}`
);
if (!response.ok) {
throw new Error('Search failed');
}
const data = await response.json();
setResults(data);
} catch (err) {
console.error('Search failed:', err);
setError('Failed to perform search. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div>
<div className="search-container">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search users..."
// Optional: Add keyboard support
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
/>
<button
onClick={handleSearch}
disabled={loading || !query.trim()}
>
{loading ? 'Searching...' : 'Search'}
</button>
</div>
{error && <div className="error-message">{error}</div>}
<div className="results-container">
{results.map(user => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
</div>
</div>
);
}
Comparing Data Fetching Methods
Here’s a comparison of different data fetching approaches:
fetch
✅ Pros:
- Built into browsers
- No additional dependencies
- Simple for basic requests
❌ Cons:
- More verbose error handling
- No request timeout by default
- Requires two-step process for JSON
async/await
✅ Pros:
- Cleaner, more readable code
- Better error handling with try/catch
- Works well with other async operations
❌ Cons:
- Requires separate function in useEffect
- Needs polyfill for older browsers
- Still uses fetch underneath
axios
✅ Pros:
- Consistent API across browsers
- Built-in request/response interceptors
- Automatic JSON transformation
- Better error handling
- Request cancellation
❌ Cons:
- Additional dependency
- Larger bundle size
- Learning curve for advanced features
API Request Lifecycle
Here’s how different data fetching methods handle the request lifecycle:

🎥 Video: Fetching APIs in React JS
Comparison of Methods
Here’s a visual comparison of the three main approaches:

Best Practices
1. Handling Loading States
Always show loading indicators to improve user experience:
function LoadingExample() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
// ... fetching logic ...
return (
<div>
{loading ? (
<LoadingSpinner />
) : (
<DataDisplay data={data} />
)}
</div>
);
}
2. Error Handling
Implement proper error boundaries and error states:
function ErrorHandlingExample() {
const [error, setError] = useState(null);
if (error) {
return (
<ErrorDisplay
message={error.message}
onRetry={() => setError(null)}
/>
);
}
// ... rest of component
}
3. Request Cancellation
Cancel pending requests when component unmounts:
function CancellableRequest() {
useEffect(() => {
const abortController = new AbortController();
fetch(url, { signal: abortController.signal })
.then(response => response.json())
.then(data => {
// Handle data
});
return () => {
abortController.abort();
};
}, []);
}
Error Handling Flow
Here’s how to implement comprehensive error handling:

🎥 Video: React Error Boundaries: How to Handle and Recover from Errors