Why? Centralises loading/error handling and enables streaming / Suspense.
If every little widget fires its own network request, you get a waterfall of spinners, duplicated calls, and tangled loading/error states. Worse, those leaf‑level fetches are hard to coordinate—try showing a single “Loading…” skeleton when five children fetch independently! Centralising data‑loading in a parent (route, page, or data layer) lets you fetch once, handle errors in one place, and stream structured data to child components.
// UsersPage.tsx – page‑level component controls data fetch & state
import { useEffect, useState } from "react";
import UsersTable from "./UsersTable";
type User = { id: number; name: string; email: string };
export default function UsersPage() {
const [users, setUsers] = useState<User[]>([]);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/users")
.then((res) => (res.ok ? res.json() : Promise.reject(res)))
.then(setUsers)
.catch(setError)
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Loading users…</p>;
if (error) return <p className="text-red-600">Failed: {error.message}</p>;
return <UsersTable users={users} />;
}
// UsersTable.tsx – presentational; receives ready‑to‑render data
type User = { id: number; name: string; email: string };
export default function UsersTable({ users }: { users: User[] }) {
return (
<table className="w-full border-collapse">
<thead>
<tr className="bg-stone-100 text-left">
<th className="p-2">Name</th>
<th className="p-2">Email</th>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.id} className="border-t">
<td className="p-2">{u.name}</td>
<td className="p-2">{u.email}</td>
</tr>
))}
</tbody>
</table>
);
}
What’s happening?
UsersPage
fetches once, manages loading/error UI, then hands clean data to UsersTable
, which remains fetch‑agnostic and easy to reuse, test, or server‑render.
// UsersTableFetchesItself.tsx – every table instance fires a request
import { useEffect, useState } from "react";
export default function UsersTableFetchesItself() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/users") // ❌ leaf‑level fetch
.then((res) => res.json())
.then(setUsers)
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Loading…</p>;
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
// Used twice on the same page? Two identical network calls run.
// Want a single skeleton? You now need cross‑component coordination.