NextjsPreloading Data Patterns

Preloading Data Patterns

As a component tree grows, data requirements often end up scattered across several nested components. If a deeply nested component only starts fetching once it renders, and something above it is also doing work before rendering that component, you can end up with an avoidable waterfall. The preload pattern lets you kick a fetch off earlier — before it is actually needed — so the data has a head start by the time it is awaited.

The preload pattern

The idea is simple: call your data-fetching function without awaiting it, as early as possible, and only await the result later where the data is actually used. Because fetch requests start executing immediately when called (the await just pauses until the Promise resolves), calling the function early lets the network request run in the background while other work happens.

TSX
// lib/data.ts
export async function getItem(id: string) {
  const res = await fetch(`https://api.example.com/items/${id}`)
  return res.json()
}

export function preloadItem(id: string): void {
  // Intentionally not awaited — just starts the request early
  void getItem(id)
}

TSX
// app/[locale]/items/[id]/page.tsx
import { getItem, preloadItem } from '@libs/data'

export default async function ItemPage({ params }: { params: { id: string } }) {
  // Kick off the fetch as early as possible in this component
  preloadItem(params.id)

  // ...do other work here, e.g. checking auth, fetching related data...
  const related = await getRelatedItems(params.id)

  // By the time we actually need the item, its request has already
  // been running in the background for a while.
  const item = await getItem(params.id)

  return <ItemDetails item={item} related={related} />
}
Note
This only provides a real benefit combined with request memoization: getItem must be deduplicated so that the later await getItem(params.id) reuses the in-flight request from preloadItem rather than firing a second one. Since preloadItem uses the same fetch call with the same arguments, request memoization makes this work automatically.
Using React's cache() for non-fetch data

The preload pattern is especially useful — and requires a bit more setup — when the underlying data source is not fetch, such as a direct database call. Wrapping the function in React's cache() gives it the same per-request memoization fetch gets automatically, which is what makes preloading safe to use without triggering duplicate database queries.

TSX
// lib/data.ts
import { cache } from 'react'
import { db } from '@libs/db'

export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } })
})

export function preloadUser(id: string): void {
  void getUser(id)
}

TSX
// app/[locale]/dashboard/layout.tsx
import { preloadUser } from '@libs/data'

export default function DashboardLayout({
  params,
  children,
}: {
  params: { userId: string }
  children: React.ReactNode
}) {
  // Start loading the user as early as the layout renders,
  // long before any deeply nested component actually awaits it.
  preloadUser(params.userId)

  return <div className="dashboard">{children}</div>
}

TSX
// app/[locale]/dashboard/settings/profile-card.tsx
import { getUser } from '@libs/data'

export default async function ProfileCard({ userId }: { userId: string }) {
  // Because of cache(), this reuses the layout's already in-flight
  // request instead of starting a brand-new database query.
  const user = await getUser(userId)

  return <p>{user.name}</p>
}
When to reach for this pattern
  • A component tree is several levels deep, and a component near the leaves needs data that could have started loading much earlier.

  • You want to start a fetch before an await for something else (e.g. an auth check) that isn’t related to that data.

  • The data source is wrapped in React’s cache() (or is a native fetch call), so the later, real await is guaranteed to reuse the preloaded request rather than duplicate it.

Tip
Preloading is a micro-optimization for larger, more deeply nested trees. For a simple page with only one or two fetches, plain async/await (optionally with Promise.all for independent requests) is clearer and sufficient — reach for preloading once prop-drilling or deep nesting makes early-fetching genuinely valuable.
Warning
Calling an async function without awaiting it and without handling its rejection can produce an unhandled promise rejection if it fails before anything awaits it. In practice this is rarely an issue for a preload that is awaited moments later, but avoid leaving a preloaded call permanently unawaited.