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.
// 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)
}// 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} />
}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.
// 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)
}// 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>
}// 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.