Fetching data from an API
Most apps get data from a server through an API — an address that returns JSON. In React the request is sent in an effect and the response goes into state. While there is no response, show "Loading…"; if the request fails, show an error message. This lesson starts the course's second example — a small blog.
A new project
Create the blog project next to the to-do list:
npm create vite@latest blog -- --template react
cd blog
npm install
npm run devPosts come from JSONPlaceholder, a free practice API. The address https://jsonplaceholder.typicode.com/posts returns an array of 100 posts with the fields id, title and body.
A simple request
Create src/components/PostList.jsx:
// src/components/PostList.jsx
import { useEffect, useState } from 'react';
export default function PostList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then((response) => response.json())
.then((data) => setPosts(data));
}, []);
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}Render <PostList /> in src/App.jsx. A moment later the page shows a list of 100 titles. The empty dependency array means the request is sent once.
Loading and errors
Let us rewrite the component with async/await and three pieces of state:
// src/components/PostList.jsx
import { useEffect, useState } from 'react';
export default function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function load() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error(`Error ${response.status}`);
}
setPosts(await response.json());
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
load();
}, []);
if (loading) return <p>Loading…</p>;
if (error) return <p>Could not load posts: {error}</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}First the page shows "Loading…", then the list. Change the address to a missing one, for example /postsx, and you see "Could not load posts: Error 404". fetch does not throw on 404 or 500, so response.ok is checked by hand.
A request that depends on a prop
If the address depends on a prop, add the prop to the dependencies and mark the old response as unwanted in the cleanup:
// src/components/PostComments.jsx
useEffect(() => {
let ignore = false;
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}/comments`)
.then((response) => response.json())
.then((data) => {
if (!ignore) setComments(data);
});
return () => {
ignore = true;
};
}, [postId]);If postId changes before the response arrives, the old response is ignored and does not overwrite the new comments.
Common mistake
The effect function cannot be declared async: it must return a cleanup function or nothing, and an async function always returns a promise.
// Wrong:
useEffect(async () => {
const response = await fetch(url);
}, []);Declare an async function inside the effect and call it, as in the example above. In real projects loading is often handed to the TanStack Query library — it is covered in the last chapter.