Sharing is caring!
A few weeks ago I opened a dashboard a friend built and watched the network tab with growing pain. Fourteen requests, one after another, each waiting for the previous. Spinners inside spinners. The page took nine seconds to become usable – on fiber. Every single request was avoidable. This post is the sequel to our React styling guide, and it covers the other half of modern React: getting data to the screen without the waterfall.

The short answer
Fetch on the server by default. Fetch on the client only for things that are interactive by nature: search boxes, filters, and mutations. If your component fetches inside useEffect on mount in 2026, that is tech debt. Full stop.
What a waterfall actually looks like
The classic pattern: component mounts, fires a request for the user, waits, then fires a request for that user orders, waits, then fires a request for each order details. Each round trip blocks the next. On localhost you never notice – 5ms per hop. In production, with 200ms per hop across three chained requests, your user stares at a skeleton for over half a second before anything real shows. Multiply by nested components and you get the nine-second dashboard.
useEffect(() => {
fetch('/api/user').then(u =>
fetch('/api/orders?user=' + u.id).then(os =>
os.forEach(o => fetch('/api/details?order=' + o.id))
)
);
}, []); // each fetch waits for the previous. Don't do this.Approach 1: Async Server Components (the default now)
Make the component itself async and fetch before anything ships to the browser. The HTML arrives with data already in it. No spinners, no waterfall, no client JavaScript spent on fetching:
async function Orders() {
const res = await fetch('https://api.shop.com/orders',
{ next: { revalidate: 60 } });
const orders = await res.json();
return <Table rows={orders} />;
}The revalidate: 60 is doing quiet heavy lifting: the result is cached for a minute, so a thousand visitors trigger one upstream request, not a thousand. I have seen this single change cut API bills in half.

Approach 2: Suspense boundaries that do not block the page
Not everything resolves at the same speed, and that is fine. Wrap each slow section in its own Suspense boundary with a skeleton that matches the final layout. The page streams: header and navigation first, then each section pops in as its data lands. The mistake I keep seeing is one giant Suspense around the whole page – that is just a full-page spinner with extra steps. One boundary per section. Always.
Approach 3: Client fetching where it actually belongs
Some things genuinely live on the client. A search-as-you-type box cannot be a server round trip per keystroke – debounce it and fetch on input. Filters, sorting, optimistic mutations (like a tweet button that flips instantly and syncs after) – all client. The rule I use: if the user did something to cause it, fetch on the client. If the page needs it to exist at all, fetch on the server.
The verdict: my rule
Server first, Suspense per section, client only for interaction. When I review React code now, I search for useEffect calls containing fetch – every hit is a conversation. Nine times out of ten it moves to the server in five minutes and a whole class of loading bugs disappears with it.
| Approach | Fetch when | Watch out |
|---|---|---|
| Server Components | The page needs the data to exist | Needs a framework with server support |
| Suspense per section | Sections resolve at different speeds | One giant boundary is a full-page spinner |
| Client fetch | The user caused it: search, filters, mutations | Never on mount for initial content |
| Client cache library | Dashboards with constant mutations | Overkill for content sites and stores |

Senior addendum: caching without tears
Two things worth knowing once the basics are in place. First, duplicate fetch calls in the same render are automatically deduplicated – call it in five components, it fires once. Second, reach for a client cache library (React Query and friends) only for dashboards with constant mutations, where data goes stale while you look at it. For content sites and stores, server cache plus revalidation wins and there is dramatically less code to maintain.
Bottom line
Part one of this series fixed how React looks. This part fixes how fast it feels: server by default, stream by section, client only for interaction. Do that and the waterfall simply has nowhere to form. Next in the series: the styling + data combo – building a real dashboard page end to end.
Part of our React in 2026 series.
