Introduction
Until now your data lived inside the app. Real apps talk: they fetch lists from a server, post new records, and handle the waiting and the failures in between. API integration is the assembly of pieces you own — fetch (JavaScript series), useEffect (5.5), state trios (5.12) — into the one pattern every data screen follows. (Roman Urdu: API integration matlab app ka bahar ki duniya se baat karna — data lana, bhejna, aur intezaar sambhalna.)
Key points
- The pattern never changes: state for
data,loading,error→ an effect (or event) that fetches → success sets data, failure sets error, both stop the loading → render one of the three states. fetchis two steps:const res = await fetch(url)gets the response;await res.json()reads the body as data. Forgetting the second step is the classic first-fetch bug.res.okis your error gate:fetchdoes not throw on a 404 or 500 — it only fails on network problems. Checkif (!res.ok) throw new Error(...)before trusting the body.(Roman Urdu: fetch 404 par bhi khamosh rehta hai — res.ok check karna aap ki zimmedari hai.)- Sending data is a POST:
fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj) })— method, the JSON content-type header, and the stringified body are the three parts that must all be present. async/awaitinside effects: an effect callback cannot itself beasync; define an async function inside (or use.thenchains) and call it. The Weather App (5.12) already used this shape.- Render the list from state: fetched arrays land in state and render with
map+ keys (5.16); new items from a POST are added to that state so the screen updates without a refetch. - Keys and URLs live in variables: the base URL (
https://jsonplaceholder.typicode.com) in one constant at the top — when the real backend replaces the practice API, one line changes.
Code examples
Example 1 — The full GET pattern: users, loaded properly
import { useEffect, useState } from "react";
const API = "https://jsonplaceholder.typicode.com";
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
async function load() {
try {
const res = await fetch(`${API}/users`);
if (!res.ok) throw new Error("Server said " + res.status);
const data = await res.json();
setUsers(data);
} catch (err) {
setError("Could not load users — check your connection.");
} finally {
setLoading(false);
}
}
load();
}, []);
if (loading) return <p>Loading users…</p>;
if (error) return <p className="error">{error}</p>;
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name} — {u.email}</li>
))}
</ul>
);
}What it does: The whole discipline in one component: the mount effect loads once, res.ok guards the body, the try/catch/finally choreographs the three states, and the renders are the early-return pattern from 5.15 feeding a keyed list from 5.16.
What you will see: "Loading users…" briefly, then ten real users with emails, straight from the practice API — and if you unplug the network and reload, the honest error line instead.
Example 2 — POST: sending a new post to the server
async function createPost(title, body) {
const res = await fetch(`${API}/posts`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, body, userId: 1 }),
});
if (!res.ok) throw new Error("Could not save the post");
return res.json();
}
// In a form's submit handler:
const saved = await createPost("My first post", "Hello from React");
console.log("Server answered with the saved post, id:", saved.id);What it does: The three POST parts line up: the method says create, the header tells the server the body is JSON, and JSON.stringify turns the object into the text that travels. The practice API answers with the saved post including a fresh id — proof the round trip worked.
What you will see: the console logs "Server answered with the saved post, id: 101" — the object you sent, returned with its server-given identity.
Example 3 — Add without refetch: POST, then update state
import { useState } from "react";
function PostBox() {
const [posts, setPosts] = useState([]);
const [title, setTitle] = useState("");
const [saving, setSaving] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setSaving(true);
const saved = await createPost(title, "Written in the React notes");
setPosts((list) => [saved, ...list]);
setTitle("");
setSaving(false);
}
return (
<div>
<form onSubmit={handleSubmit}>
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Post title" />
<button disabled={saving}>{saving ? "Saving…" : "Add post"}</button>
</form>
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
</div>
);
}What it does: After the POST succeeds, the returned post is prepended to the local state — the list updates instantly with no second fetch. The button shows its own mini loading state ("Saving…") and disables itself against double-submits — the small professional touches real apps are made of.
What you will see: type a title, press Add — "Saving…" flashes, then the new post appears at the top of the list, and the input clears for the next one.
Quick recap
- Every API screen is the same skeleton: data / loading / error state + fetch + the three renders.
fetchis two steps with a manualres.okgate; POST needs method + JSON header +JSON.stringifybody, all three.- After a successful POST, update local state with the server's answer instead of refetching the world.
Common mistakes
- Forgetting
await res.json()— the component renders a Promise where data should be, or crashes reading fields of it. The fix: two awaits, always — response first, body second. - Trusting fetch to throw on 404 — it does not; the error JSON body gets treated as your data and the screen shows garbage or crashes. The fix:
if (!res.ok) throw …before any.json()you trust. - Fetching in the render body — a fetch written directly in the component runs on every render, and its setState triggers another render: an infinite request storm. The fix: fetches live in effects (for loading) or handlers (for actions) — never loose in the render path.
Practice
- Build a
TodoLoaderon the practice API (/todos?_limit=8): render each todo with its title and a ✓/○ forcompleted. Add a refresh button that re-runs the load (extract the load function so both the effect and the button call it), with the loading state showing during refreshes too. - Extend Example 3: give each listed post a Delete button that removes it from state (practice APIs accept DELETE but forget instantly — your job is the local state removal + a
deletingindicator on that row while the call runs).