Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Core 13 min read

Fetching data

await in the component – and how to avoid waterfalls

In one sentence

In a server component you fetch with await, right there. No useEffect, no loading state, no race – but a new trap: loads that run one after another for no reason.

This is where Next.js is most obviously simpler than plain React. What took three states and an effect there is one line here.

The same job, two worlds
plain React in the browser
"use client";

function List() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let current = true;
    fetch("/api/posts")
      .then((r) => r.json())
      .then((d) => current && setData(d))
      .catch(setError)
      .finally(() => setLoading(false));
    return () => { current = false; };
  }, []);

  if (loading) return <p>Loading …</p>;

}
server component
export default async function List() {
  const posts = await db.post.findMany();

  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

No loading state, because the user only sees the page once the data is there. No race, because nothing runs in parallel in the browser. And no API in between, because you are already on the server.

Good to know Where the data comes from does not matter

fetch to a third-party API, a database call, the file system – all allowed. The code runs on the server, so the server's possibilities apply.

The new trap: the waterfall

Two awaits in a row mean the second only starts when the first finishes. If they know nothing about each other, that is wasted time – a WaterfallTwo loads running one after the other although they could run side by side – the most common slowdown..

sequential: 300 ms + 300 ms
const user = await loadUser(id);
const posts = await loadPosts();   // waits for no reason
at the same time: 300 ms
const [user, posts] = await Promise.all([
  loadUser(id),
  loadPosts(),
]);

Sequential is only right when the second call needs the first one's result – loadOrders(user.id), for instance.

Tip The more elegant route: do not wait at all

Instead of collecting everything before anything appears, you can let each part fetch for itself and wrap it in <Suspense>. Then every area shows up as soon as it is ready. There is a lesson on streaming for that.

The same request more than once

If the layout and the page both need the signed-in user, you simply write the call twice. Next.js still only runs it once – that is Request memoizationThe same `fetch` used several times while building one page only really runs once..

memoization.tsx
// app/layout.tsx
const user = await loadUser();     // performs the request

// app/page.tsx
const user = await loadUser();     // reuses the same result

// Automatic for fetch. For your own functions:
import { cache } from "react";
export const loadUser = cache(async () => { … });
This removes the need to thread data down through several levels.

Where data belongs

Reach for it when …
  • +

    In the component that displays it – even deep in the tree

  • +

    In the layout, if it applies to the whole area (menu, user)

  • +

    In a shared function wrapped in cache() when several need it

Skip it when …
  • In a useEffect – that is the detour you just got rid of

  • In your own API route, only to call it yourself afterwards

  • Right at the top, then threaded down through five levels

Pitfall No custom API for your own server

A common reflex: build a route.ts first, then call it from the server component with fetch("/api/…"). That is a network detour to yourself. Call the function directly. You only need a Route handlerA `route.ts` that answers an address without being a page – your way to a custom API. when something outside wants access.

In more depth: client components that need data optional

A client component may not be async and cannot touch the database. It has three routes – in this order:

routes.txt
1. Pass it down
   A server component fetches and hands the result over as a prop.
   Covers the vast majority of cases.

2. Call a server action
   For data only needed once the user does something.

3. Fetch in the browser
   If it genuinely has to be client-side: TanStack Query or similar
   against a route handler.
Tip Rule out route 1 first

Very often the boundary can simply be drawn elsewhere: the client component gets smaller, the fetching stays on the server. That is almost always better than a second loading layer in the browser.

Does it stick?

5 questions on this lesson. Wrong answers show up in your stats.

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →