Runnable lesson · press Try it on the examples to edit and run them here.
fetch is built into every modern browser, so you can call APIs from React with no library. This lesson shows the two shapes you will write most: GET to load data and POST to send it. (Revisit The Fetch API in the JavaScript course for the basics.)
In the playground, requests to other websites are blocked, so the examples use fetch against data: URLs and a small simulated server. The code is the same as with a real API, apart from the URL.
GET: load data when the component appears
// A "server response" as a data URL
const url = "data:application/json," + encodeURIComponent(JSON.stringify([
{ id: 1, name: "Ada Lovelace" },
{ id: 2, name: "Grace Hopper" },
]));
function App() {
const [users, setUsers] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
let ignore = false;
async function load() {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Request failed: " + response.status); // fetch does not throw on 404 or 500
}
const data = await response.json();
if (!ignore) setUsers(data);
} catch (err) {
if (!ignore) setError(err.message);
}
}
load();
return () => { ignore = true; };
}, []);
if (error) return <p role="alert">Error: {error}</p>;
if (!users) return <p>Loading</p>;
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
Things to copy into your own code:
- An async function inside the effect, because the effect function itself cannot be
async. if (!response.ok):fetchonly rejects on network failure. A404or500still resolves, so you must check the status.ignoreflag in cleanup to avoid updating state after the component is gone or a newer request started. See Fetching Data with Effects.try/catchfor both network errors and your own thrown errors.
With a real API, only the URL and options change:
const response = await fetch("https://api.example.com/users");
POST: send data from an event handler
Requests that change data belong in event handlers (a click or a submit), not in effects. Set the method, a JSON content type and a JSON body:
async function createUser(user) {
const response = await fetch("https://api.example.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(user),
});
if (!response.ok) throw new Error("Could not create user: " + response.status);
return response.json();
}
Here is the shape with a simulated server, in a complete form:
// Simulated server: stores users in memory and answers after a short delay
const db = [{ id: 1, name: "Ada" }];
function fakePost(user) {
return new Promise((resolve) =>
setTimeout(() => {
const saved = { id: db.length + 1, ...user };
db.push(saved);
resolve(saved);
}, 500)
);
}
function App() {
const [users, setUsers] = React.useState(db);
const [name, setName] = React.useState("");
const [saving, setSaving] = React.useState(false);
async function handleSubmit(e) {
e.preventDefault();
if (!name.trim()) return;
setSaving(true);
try {
const saved = await fakePost({ name: name.trim() });
setUsers((list) => [...list, saved]); // update the screen with the server's answer
setName("");
} finally {
setSaving(false); // always leave the busy state
}
}
return (
<div>
<form onSubmit={handleSubmit}>
<input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} />
<button type="submit" disabled={saving}>{saving ? "Adding..." : "Add user"}</button>
</form>
<ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
</div>
);
}
Keep requests in their own functions
Move fetch calls into plain functions, outside components. They are easier to test, reuse and change (for example when you add an auth header):
// api.js
const BASE = import.meta.env.VITE_API_URL;
export async function getJson(path, options) {
const response = await fetch(BASE + path, options);
if (!response.ok) {
throw new Error(`Request to ${path} failed with ${response.status}`);
}
return response.json();
}
Components then call getJson("/users"). The environment variable is explained in Environment Variables.
What fetch does not do for you
- Timeouts. A request can hang; use
AbortSignal.timeout(ms)or anAbortController; see Aborting Requests. - Caching, retries and deduplication. Data libraries provide them; see Data-Fetching Libraries.
- Sending cookies to other origins. That needs
credentials: "include"and matching CORS headers on the server.
Key points
- Load data in an effect with an async inner function; send data from event handlers.
- Check
response.ok;fetchdoes not reject on HTTP errors. - Put request code in plain functions, and handle failures.