Introduction
Working with APIs is a core part of building real-world React applications. Whether you are fetching user data, product lists, or dashboard stats, it's important to handle API calls correctly. Poorly managed API logic can cause bugs like repeated API calls, missing loading spinners, or inconsistent UI. In this article, you will learn how to make API calls in React using clean, simple code — and how to manage loading, success, and error states using best practices.
Why Handling API Calls Correctly Matters
Incorrect API handling can lead to:
UI flickering or blank screens
Multiple API calls due to re-renders
Errors not showing to the user
Loading states not updating correctly
Memory leaks when using async functions inside useEffect
With proper structure, you can keep your React UI smooth, predictable, and error-free.
Basic API Call in React Using useEffect
The most common way to fetch data in React is using useEffect and fetch or axios.
Example
import { useEffect, useState } from "react";
function App() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("https://api.example.com/products")
.then(response => response.json())
.then(result => {
setData(result);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
return <div>...</div>;
}
What This Code Does
loading begins as true
API request runs once (because dependency array is empty)
If successful → stores data and hides loading
If error → shows error message and hides loading
This structure ensures a clean and predictable UI.
Using Async/Await for Cleaner API Code
Using .then() works fine but async/await is easier to read.
Example
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch("https://api.example.com/users");
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
Why This Is Better
Showing a Proper Loading Spinner
Users should not see a blank page while data loads.
Example UI
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
Why This Helps
You can replace <p>Loading...</p> with a real spinner component.
Handling Errors Correctly
API errors are common — wrong URL, network failure, server down.
Display Error to User
if (error) return <div>Something went wrong: {error}</div>;
Example of UI After Error
Clear error messages guide users, instead of showing a broken screen.
Preventing Multiple API Calls (Important)
React may re-render components often. If your API call is inside useEffect without proper dependency control, it may fire multiple times.
Common Mistake
useEffect(() => {
fetchData();
});
This runs on every render.
Correct Approach
useEffect(() => {
fetchData();
}, []);
Now it runs once, on initial render.
Canceling API Calls to Avoid Memory Leaks
When leaving a page during an API call, React may show warnings like:
Can't perform a React state update on an unmounted component
To avoid this, use an abort controller.
Example
useEffect(() => {
const controller = new AbortController();
const loadUsers = async () => {
try {
const res = await fetch("https://api.example.com/users", {
signal: controller.signal,
});
const json = await res.json();
setData(json);
} catch (err) {
if (err.name !== "AbortError") {
setError(err.message);
}
}
};
loadUsers();
return () => controller.abort();
}, []);
Why This Matters
Using Custom Hooks for Clean and Reusable API Logic
To avoid repeating loading, error, and fetching logic, create a custom hook.
Custom Hook Example: useFetch
import { useEffect, useState } from "react";
export function useFetch(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);
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
Using the Hook
const { data, loading, error } = useFetch("https://api.example.com/posts");
This keeps your components clean and readable.
Best Practices for API Handling in React
Always show loading and error states
Use async/await for readable code
Prevent multiple API calls using dependency arrays
Clean up API calls using AbortController
Extract logic into custom hooks for reusability
Avoid updating UI before data is ready
Validate API responses before using them in UI
Conclusion
Handling API calls the right way is crucial for building professional and stable React applications. By managing loading and error states, preventing unnecessary API calls, cleaning up requests, and using reusable hooks, you can create a smooth user experience and avoid common bugs. With these techniques, your React apps will load data correctly, show helpful feedback, and perform better overall.