John Aleman

Colocating Data Fetching in Server Components

Fetching data on the server, next to the component that needs it, so the initial page renders without the browser making its own round of API requests.

View the live demo →
t: "server components"

Fetch data where the component is defined

Server Components + data colocation means fetching data directly where a component is defined and consumed, with that work happening on the server. The Server Component handles data access, transformation, and the initial render; a Client Component takes over only where the page needs interactivity.

Keeping the fetch next to the component that renders it makes the component's data requirements easy to read and to maintain — what it needs is declared right where it's used, not assembled by a distant loader and threaded down through props.

t: "the round trip"

Skip the browser's trip back to your own API

With traditional client-side fetching, the browser has to paint an empty page before it can even start asking for data — and every step after that is a network hop or a chunk of JavaScript the user is waiting on:

Browser:
render page → run JavaScript → request /api
            → server queries database → data returns → render data

A Server Component moves that initial data work to the server, so the browser receives finished UI instead of a list of things to go and fetch:

Server Component → fetch data → render UI → Browser

Think of a construction site. Instead of sending the customer into the warehouse to gather every material a room needs, the crew gathers and prepares the materials on site and delivers the finished structure. The Server Component is the crew doing the work, the database is the warehouse, the rendered UI is the finished structure, and a Client Component is the part of it the customer can actually interact with.

Don't send the customer into the warehouse — the crew builds the room and hands it over ready to use.

t: "colocation"

Data-fetching lives next to the component

Colocation means each section owns its own fetch instead of a single top-level loader assembling one giant payload for the whole page. The component that renders the numbers is also the component that asks for them:

Dashboard
├── UserHeader      → getWorkspaceOwner()
├── MetricsGrid     → getWorkspaceMetrics(period)
└── RecentActivity  → getRecentActivity(limit)

That keeps the data-fetching logic — and the cognitive overhead of following it — right beside the UI it feeds. A data-access function sits next to its component and runs on the server:

// data-access function, colocated with the component
export async function getWorkspaceOwner(): Promise<WorkspaceOwner> {
  const data = await runQuery<{ owner: WorkspaceOwner }>(`
    query WorkspaceOwner {
      owner { name email plan planStatus }
    }
  `);
  return data.owner;
}

Shared queries still factor out into reusable server-side functions; colocation is about where a fetch is called, not about duplicating the query behind it.

t: "client boundary"

Rendering on the server, interactivity on the client

Server Components do data access, transformation, and the initial render; Client Components — the ones marked "use client" — own state, event handlers, and browser APIs. Push that boundary as low as it'll go: in the demo the period control is the single client component, and the dashboard around it stays on the server.

Server Component ── data + rendered UI ──▶ sent to the browser
                                        │
                                        └─▶ PeriodPicker ("use client")
                                            state · events · browser APIs

Because the data work stays on the server, sensitive operations stay there too — database queries, authentication checks, and credentials never reach the browser. Server Components can read databases, environment variables, and files directly, without a browser-facing API layer or CORS to stand up in front of them.

t: "orchestrate & stream"

Coordinate the data, stream the slow parts

A single Server Component can coordinate several data requirements and apply whatever business logic ties them together. It doesn't have to make the whole page wait on the slowest one, though: wrap each async section in its own <Suspense> and its fallback ships with the initial response, with the real content streaming in when that section's data resolves.

<Suspense fallback={<LoadingSpinner text="Loading metrics..." />}>
  <MetricsGrid period={activePeriod} />
</Suspense>

<Suspense fallback={<LoadingSpinner text="Loading recent activity..." />}>
  <RecentActivity limit={4} />
</Suspense>

The header, which needs no slow data, renders instantly outside any boundary; metrics and activity each arrive on their own timer. It's the pattern behind a real dashboard — profile, subscription, recent activity, analytics — where most sections just display data and only a filter or button needs to be interactive.

t: "example"

The dashboard demo's structure

Here's how the live demo fits together: a colocated data layer, a Server Component page that fetches the shared record and delegates the rest, three presentational sections, and one client component for the only interactive control.

app/patterns/colocating-server-data/
├── data.ts                 ← in-process graphql-yoga schema + queries
└── dashboard/
    ├── layout.tsx          ← static shell: header, workspace name
    └── page.tsx            ← fetches the owner, wraps the slow sections

components/patterns/colocating-server-data/
├── UserHeader.tsx          (prop-driven, no fetch)
├── MetricsGrid.tsx         (server fetch: getWorkspaceMetrics)
├── RecentActivity.tsx      (server fetch: getRecentActivity)
└── PeriodPicker.tsx        ("use client" — the metrics window)

components/patterns/loading-spinner.tsx  ← shared Suspense fallback

The data layer speaks GraphQL. data.ts builds a small executable schema with graphql-yoga's createSchema and runs real queries against it — the in-process twin of the site'sgraphqlRequest against /api/graphql, so the demo needs no database to run.

Each section fetches what it renders. MetricsGrid and RecentActivity call their own data-access functions on the server; UserHeader takes its data as a prop and never fetches at all.

Remember: Server Components do the data work, Client Components provide the interactivity, and the data-fetching logic lives with the component that needs it.