React applications commonly communicate with backend APIs. To understand API calls in React, you need a solid understanding of JavaScript asynchronous programming, especially Promises and async/await.
1 Synchronous vs Asynchronous
Synchronous code generally runs one operation after another. Asynchronous code allows a long-running operation to complete later without blocking the JavaScript execution flow in the same way.
Network requests are a common example of asynchronous work.
2 What Is a Promise?
A Promise represents the eventual result of an asynchronous operation. A Promise can be pending, fulfilled, or rejected.
flowchart TD
A[Promise Created] --> B[Pending]
B --> C[Fulfilled]
B --> D[Rejected]
3 Basic Promise
const promise = new Promise(
(resolve, reject) => {
const success = true;
if (success) {
resolve("Operation completed");
} else {
reject("Operation failed");
}
}
);
4 then and catch
promise
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error);
});
then() handles a successful result. catch() handles a rejection.
5 fetch()
The browser provides the Fetch API for making HTTP requests.
fetch("/api/users")
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
6 What Does fetch Return?
fetch() returns a Promise for a Response object. The response body must then be read, for example with response.json().
fetch("/api/users")
.then(response => {
return response.json();
})
.then(data => {
console.log(data);
});
7 async and await
async and await provide a cleaner syntax for working with Promises.
async function loadUsers() {
const response =
await fetch("/api/users");
const data =
await response.json();
return data;
}
An async function always returns a Promise.
8 Error Handling with try/catch
async function loadUsers() {
try {
const response =
await fetch("/api/users");
if (!response.ok) {
throw new Error(
"Request failed"
);
}
const data =
await response.json();
return data;
}
catch (error) {
console.error(error);
}
}
9 Promise vs async/await
| Promises | async/await |
|---|---|
| Uses then/catch | Looks more like synchronous code |
| Useful for chaining | Often easier for sequential operations |
| Still fundamental underneath | Built on top of Promises |
10 API Calls in React
When a component needs data from an API after rendering, React commonly uses an effect to start the request.
import { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
async function loadUsers() {
try {
const response =
await fetch("/api/users");
if (!response.ok) {
throw new Error(
"Unable to load users"
);
}
const data =
await response.json();
setUsers(data);
}
catch (error) {
setError(error.message);
}
finally {
setLoading(false);
}
}
loadUsers();
}, []);
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>{error}</p>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
</li>
))}
</ul>
);
}
11 API Call Flow in React
flowchart TD
A[Component Renders] --> B[Effect Starts]
B --> C[API Request]
C --> D{Response}
D -->|Success| E[Parse JSON]
D -->|Failure| F[Handle Error]
E --> G[Update State]
F --> H[Set Error State]
G --> I[Render Data]
H --> J[Render Error]
12 HTTP Status Check
A common mistake is assuming that fetch() rejects automatically for every HTTP error. For HTTP responses such as 404 or 500, you should normally check response.ok or the status code.
const response =
await fetch("/api/users");
if (!response.ok) {
throw new Error(
`HTTP error: ${response.status}`
);
}
13 POST Request
async function createUser(user) {
const response = await fetch(
"/api/users",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(user)
}
);
if (!response.ok) {
throw new Error(
"Unable to create user"
);
}
return response.json();
}
14 Loading, Error, and Data States
A real API component normally needs at least three states:
- Loading
- Error
- Data
There may also be an empty-data state.
flowchart TD
A[Start Request] --> B[Loading]
B --> C{Request Result}
C -->|Success With Data| D[Show Data]
C -->|Success Empty| E[Show Empty State]
C -->|Failure| F[Show Error]
15 Avoiding an async Effect Callback
For a simple effect, instead of making the effect callback itself async, define an async function inside it and call that function.
useEffect(() => {
async function loadData() {
const response =
await fetch("/api/data");
const data =
await response.json();
setData(data);
}
loadData();
}, []);
16 Complete Example — Search by User
function UserSearch() {
const [search, setSearch] = useState("");
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
async function searchUsers() {
setLoading(true);
setError("");
try {
const response = await fetch(
`/api/users?search=${encodeURIComponent(search)}`
);
if (!response.ok) {
throw new Error(
"Unable to search users"
);
}
const data = await response.json();
setUsers(data);
}
catch (error) {
setError(error.message);
}
finally {
setLoading(false);
}
}
return (
<div>
<input
value={search}
onChange={event =>
setSearch(event.target.value)
}
/>
<button onClick={searchUsers}>
Search
</button>
{loading && <p>Loading...</p>}
{error && <p>{error}</p>}
{users.map(user => (
<p key={user.id}>
{user.name}
</p>
))}
</div>
);
}
17 Common Mistakes
- Forgetting that
fetch()returns a Promise. - Forgetting to call
response.json()for JSON responses. - Not checking
response.ok. - Ignoring loading and error states.
- Making the effect callback itself async without understanding the cleanup contract.
- Updating state after a component is no longer interested in the request result.
- Starting API requests repeatedly because the effect dependencies are incorrect.
18 Interview Questions
- What is a Promise?
- What are the states of a Promise?
- What is the difference between then/catch and async/await?
- What does an async function return?
- Does fetch reject automatically for HTTP 404 or 500?
- Why do we call response.json()?
- How do you handle API calls in React?
- Why should API components handle loading and error states?
- How do you send a POST request using fetch?
19 Key Takeaways
- Promises represent future asynchronous results.
async/awaitprovides readable Promise-based code.fetch()returns a Promise.- Check
response.okfor HTTP failures. - React API components commonly manage loading, error, and data states.
- Use an inner async function when making asynchronous work from an effect.
Conclusion
React does not replace JavaScript's asynchronous model. React applications still use Promises, async/await, fetch, and normal JavaScript error handling. Understanding these fundamentals is essential for building real applications that communicate reliably with backend APIs.