A common pattern in React is to fetch data when a component mounts.
This usually means we need three state values:
-
loading: whether the request is in progress -
error: whether something went wrong -
data: the result of the request
import { useEffect, useState } from 'react';
type Post = {
userId: number;
id: number;
title: string;
body: string;
};
export default function PostList() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchPosts = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const data = (await response.json()) as Post[];
setPosts(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Something went wrong');
} finally {
setLoading(false);
}
};
fetchPosts();
}, []);
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error: {error}</p>;
}
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<strong>{post.title}</strong>
</li>
))}
</ul>
);
}
1) Why this pattern is useful
We use useEffect here because the request is a side effect: it happens after the component renders.
The flow is:
- component renders
-
useEffectstarts the request - while loading, we show a loading state
- if the request fails, we set an error message
- after success or failure, we stop loading
2) Error handling
There are two common cases where a fetch can fail:
- the network request itself throws an error
- the server responds with a non-OK status, such as
404or500
Both cases should be handled in a try/catch block.
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
This makes the error explicit and lets the catch block handle both kinds of errors consistently.
3) finally
finally runs no matter whether the request succeeds or fails.
This is the best place to stop the loading state:
} finally {
setLoading(false);
}
That way, the UI always exits the loading state after the request completes.
4) Summary
When fetching data in React:
- create loading, error, and data state
- start the request in
useEffect - use
try/catchto handle failures - manually throw when the HTTP response is not ok
- stop loading in
finally
This pattern is the standard starting point for working with async data in React components.
Top comments (0)