Skip to main content

Command Palette

Search for a command to run...

React API Integration – Fetching & Managing Data

Updated
12 min readView as Markdown

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 userId changes, 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.


3 views

100 Days of Code: My Journey to Becoming a Full Stack Developer

Part 1 of 50

Welcome to my 100 Days of Code journey! In this series, I'll document my daily progress as I learn Full Stack Web Development from the ground up. Every post will cover what I learned, challenges I faced, mistakes I made, and the projects I built. My goal is not just to complete 100 days but to become a better developer through consistency, discipline, and learning in public. Topics I'll cover include: • Git & GitHub • HTML, CSS & JavaScript • React.js • Node.js & Express • MongoDB • APIs • Real-world Projects • AI tools for Developers Whether you're just starting out or revising your fundamentals, I hope this journey helps you learn alongside me. Let's build, learn, and grow together! 🚀