React API Integration – Fetching & Managing Data
A frontend application becomes much more powerful when it can communicate with a backend.
Until now, most of our React applications have worked with data that already exists inside the component:
const users = [
{ id: 1, name: "Saurabh" },
{ id: 2, name: "Rahul" }
];
But real applications don't usually work this way.
Instead:
React Frontend
↓
HTTP Request
↓
Backend / API
↓
Database
↓
Backend Response
↓
React Frontend
↓
UI
Today we're learning how to build this connection.
🔹 What Is API Integration?
API integration means allowing one application to communicate with another system through an API.
For a React application, this commonly means communicating with:
Frontend
↓
REST API
↓
Backend
↓
Database
For example, imagine a MERN application:
React
↓
Express.js
↓
Node.js
↓
MongoDB
React doesn't directly communicate with MongoDB.
Instead, React communicates with your backend API.
React → Express/Node → MongoDB
React ← Express/Node ← MongoDB
This architecture is extremely important for the MERN stack.
🔹 What Is an HTTP Request?
When React wants information from a server, it sends an HTTP request.
Common HTTP methods include:
| Method | Purpose |
|---|---|
| GET | Retrieve data |
| POST | Create data |
| PUT | Replace/update data |
| PATCH | Partially update data |
| DELETE | Delete data |
For example:
GET /api/users
might return:
[
{
"id": 1,
"name": "Saurabh"
},
{
"id": 2,
"name": "Rahul"
}
]
🔹 Using fetch()
JavaScript provides the fetch() API for making HTTP requests.
Basic example:
fetch("https://example.com/api/users");
But fetch() returns a Promise.
Therefore we can use:
fetch("https://example.com/api/users")
.then(response => response.json())
.then(data => {
console.log(data);
});
Or, preferably, with async/await:
async function getUsers() {
const response = await fetch(
"https://example.com/api/users"
);
const data = await response.json();
console.log(data);
}
🔹 Why Use useEffect?
Fetching data is a side effect.
That's why it commonly happens inside useEffect.
useEffect(() => {
async function fetchUsers() {
const response = await fetch(
"https://example.com/api/users"
);
const data = await response.json();
setUsers(data);
}
fetchUsers();
}, []);
The empty dependency array means the effect runs when the component mounts.
🔹 The Basic React Data-Fetching Pattern
A typical component needs three pieces of state:
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
Why three?
Because the request can be in different states:
Loading
↓
Success
OR
Loading
↓
Error
This is one of the most important patterns to understand.
🔹 Building Our First API Component
Let's create a simple user component.
import { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUsers() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
setUsers(data);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
}
fetchUsers();
}, []);
if (loading) {
return <h2>Loading...</h2>;
}
if (error) {
return <h2>Error: {error}</h2>;
}
return (
<div>
<h1>Users</h1>
{users.map(user => (
<div key={user.id}>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
))}
</div>
);
}
export default Users;
Let's break down what is happening.
🔹 Step 1 — Initial State
const [users, setUsers] = useState([]);
Initially:
users = []
Then:
const [loading, setLoading] = useState(true);
means:
loading = true
And:
const [error, setError] = useState(null);
means:
error = null
🔹 Step 2 — Component Renders
Initially:
loading = true
Therefore:
if (loading) {
return <h2>Loading...</h2>;
}
The user sees:
Loading...
🔹 Step 3 — API Request
After rendering, useEffect executes.
fetch("https://jsonplaceholder.typicode.com/users")
The browser sends the request.
🔹 Step 4 — Server Responds
The server returns data.
For example:
[
{
"id": 1,
"name": "Leanne Graham",
"email": "leanne@example.com"
}
]
We convert the response into JavaScript:
const data = await response.json();
🔹 Step 5 — Update State
setUsers(data);
Now React receives the data.
State changes:
[]
↓
[users]
The component re-renders.
🔹 Step 6 — Loading Ends
setLoading(false);
Now:
loading = false
So the loading UI disappears.
🔹 Step 7 — Render Data
React now executes:
users.map(user => (
<div key={user.id}>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
))
And the users appear.
🔹 The Complete Flow
This is worth memorizing conceptually:
Component renders
↓
loading = true
↓
"Loading..."
↓
useEffect executes
↓
API request
↓
Server responds
↓
setUsers(data)
↓
setLoading(false)
↓
Re-render
↓
Display users
This is the foundation of data-driven React applications.
🔹 Why response.ok Matters
A common beginner mistake is:
const response = await fetch(url);
const data = await response.json();
without checking the response.
Remember:
fetch()doesn't automatically reject the Promise for HTTP errors such as 404 or 500.
Therefore:
if (!response.ok) {
throw new Error("Something went wrong");
}
is important.
🔹 Handling Errors
Let's make our error UI better.
if (error) {
return (
<div>
<h2>Something went wrong.</h2>
<p>{error}</p>
</div>
);
}
Now our application has:
Loading State
Success State
Error State
🔹 Empty State
What if the API successfully returns an empty array?
[]
That's not an error.
It's a successful request with no data.
We can handle it separately:
if (users.length === 0) {
return <p>No users found.</p>;
}
Now our UI has four meaningful states:
Loading
↓
Success → Data
↓
Empty → No data
OR
Error → Request failed
This is much closer to production-quality UI.
🔹 Fetching Based on an ID
Now let's make the request dynamic.
Suppose we have:
function User({ userId }) {
We want to fetch a specific user.
useEffect(() => {
async function fetchUser() {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const data = await response.json();
setUser(data);
}
fetchUser();
}, [userId]);
Notice:
[userId]
The effect depends on userId.
If:
userId = 1
we fetch:
/users/1
If it changes:
userId = 2
React runs the effect again:
/users/2
🔹 Why Dependencies Matter
Consider:
useEffect(() => {
fetchUser(userId);
}, [userId]);
The dependency array tells React:
"Whenever
userIdchanges, synchronize the component with the corresponding user."
This is exactly what we learned yesterday about effects.
Today we're applying it to API calls.
🔹 POST Request
GET retrieves data.
But what about creating data?
We use POST.
async function createUser() {
const response = await fetch(
"https://example.com/api/users",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Saurabh",
email: "saurabh@example.com"
})
}
);
const data = await response.json();
console.log(data);
}
The important pieces are:
method: "POST"
and:
headers: {
"Content-Type": "application/json"
}
and:
body: JSON.stringify(data)
🔹 Why JSON.stringify()?
JavaScript object:
const user = {
name: "Saurabh",
age: 23
};
HTTP request bodies commonly send JSON text.
Therefore:
JSON.stringify(user)
produces:
{"name":"Saurabh","age":23}
On the backend, this JSON can be parsed back into an object.
🔹 React Form + API
Now let's connect concepts from previous days.
function Register() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
async function handleSubmit(event) {
event.preventDefault();
const response = await fetch(
"https://example.com/api/users",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name,
email
})
}
);
const data = await response.json();
console.log(data);
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={event => setName(event.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={event => setEmail(event.target.value)}
placeholder="Email"
/>
<button type="submit">
Register
</button>
</form>
);
}
Look at how many React concepts we've combined:
Controlled Forms
+
State
+
Events
+
API
+
POST
This is exactly how your earlier React knowledge starts becoming practical.
🔹 PATCH Request
Suppose we want to update only part of a resource.
await fetch(`/api/users/${userId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Updated Name"
})
});
PATCH is commonly used for partial updates.
🔹 DELETE Request
To delete something:
await fetch(`/api/users/${userId}`, {
method: "DELETE"
});
Then we can update our local state.
For example:
setUsers(prevUsers =>
prevUsers.filter(user => user.id !== userId)
);
Notice that we're not necessarily making another GET request.
We can update the UI using our existing state.
🔹 API Integration With Your MERN Stack
This is where today's topic becomes especially important.
Imagine you eventually build:
React Frontend
↓
Axios / Fetch
↓
Express API
↓
Controller
↓
MongoDB
For example:
POST /api/auth/register
React sends:
{
"name": "Saurabh",
"email": "saurabh@example.com",
"password": "password"
}
Express receives it.
Then:
Express
↓
Validate
↓
Controller
↓
MongoDB
↓
Response
React receives:
{
"message": "User registered successfully"
}
This is the bridge between your React learning and your eventual MERN projects.
🔹 Fetch vs Axios
You can make API requests using:
fetch
or libraries such as:
Axios
For now, understanding fetch() is extremely valuable because it is built into modern browsers.
Once the fundamentals are clear, learning Axios becomes much easier.
🔹 Avoid Putting API Logic Everywhere
Instead of doing this repeatedly:
fetch("/api/users")
inside many components, larger applications often separate API logic.
For example:
src/
├── components/
├── pages/
├── hooks/
├── services/
└── api/
You might create:
// api/userApi.js
export async function getUsers() {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
return response.json();
}
Then your component can use:
useEffect(() => {
async function loadUsers() {
const data = await getUsers();
setUsers(data);
}
loadUsers();
}, []);
This separation makes applications easier to maintain.
🔹 Environment Variables
You shouldn't hard-code every backend URL throughout your application.
For example:
fetch("http://localhost:5000/api/users");
During deployment, your backend may have a different URL.
Instead, frontend projects can use environment variables.
With Vite, client-exposed environment variables use the VITE_ prefix.
For example:
VITE_API_URL=http://localhost:5000
Then:
const API_URL = import.meta.env.VITE_API_URL;
fetch(`${API_URL}/api/users`);
This becomes useful when moving from:
Local Development
↓
Production
🔹 CORS
While working with your MERN stack, you will eventually encounter:
CORS
Suppose:
React
localhost:5173
communicates with:
Express
localhost:5000
These are different origins.
The browser may block the request unless the backend allows the frontend origin.
In Express, this is commonly handled with the cors package.
For example:
import cors from "cors";
app.use(cors());
CORS is a browser security mechanism, not a React feature.
Understanding this now will save you a lot of confusion when connecting your React frontend to your Express backend.
🔹 Common API Mistakes
❌ Forgetting await
const data = response.json();
Instead:
const data = await response.json();
❌ Forgetting response.ok
const response = await fetch(url);
Always consider:
if (!response.ok) {
throw new Error("Request failed");
}
❌ Updating state after assuming success
Don't blindly assume every request succeeds.
Handle:
Loading
Success
Error
Empty
❌ Putting async directly on useEffect
Avoid:
useEffect(async () => {
// ...
}, []);
Instead:
useEffect(() => {
async function fetchData() {
// ...
}
fetchData();
}, []);
❌ Hard-coding API URLs everywhere
Prefer a centralized configuration/API layer as your application grows.
🔹 Best Practices
✅ 1. Always handle loading
if (loading) {
return <Loading />;
}
✅ 2. Handle errors
catch (error) {
setError(error.message);
}
✅ 3. Check HTTP status
if (!response.ok) {
throw new Error("Request failed");
}
✅ 4. Keep API logic organized
Separate API/service logic when your project grows.
✅ 5. Keep UI state separate from server data
Don't mix every piece of application state into one giant object.
✅ 6. Use stable dependencies
When fetching based on something such as userId, include it:
[userId]
✅ 7. Think about race conditions
If requests can overlap, consider cancellation or a strategy that ensures an older response doesn't overwrite newer data.
🧠 My Biggest Takeaway
Today I learned that React isn't just about creating UI — it's about creating UI that communicates with the real world.
The important pattern I learned is:
React State
↓
useEffect
↓
API Request
↓
Server
↓
Response
↓
setState()
↓
Re-render
I also understood how GET, POST, PATCH, and DELETE requests fit into frontend development, how to handle loading/error/empty states, and how React eventually connects to an Express + MongoDB backend.
This is a major step toward building my own full-stack MERN applications.
