Next.js Data Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
await fetch(...) directly in the Server Component body instead of reaching for useEffect - no loading state, no client waterfall, and the data ships as HTML.await yields a Promise object (often rendered as [object Promise]); always do const { slug } = await params.const [user, posts] = await Promise.all([getUser(id), getPosts(id)]) - so total latency equals the slowest call instead of the sum, and let TypeScript infer the result tuple for free.const a = await getA(); const b = await getB(); for independent data - promises start at creation, but each await still blocks the next line, producing sequential latency.Promise.allSettled prevents a single rejection from discarding the other successful results and lets you render a partial page.fetch API, so wrap database and ORM functions - const getUser = cache((id: string) => db.user.findUnique({ where: { id } })) - to get per-render deduplication across layouts and pages.cache: "no-store" anywhere in a route opts the whole route into dynamic rendering; move it into a separate component wrapped in <Suspense> so the rest can stay static.cache: "auto" (not force-cache), so set cache or next.revalidate on every fetch rather than relying on whatever the framework infers.fetch(url, { next: { tags: ["product-abc123"] } }) - use namespaced strings instead of generic "data" so revalidateTag("product-abc123") does not quietly blow away unrelated caches.revalidatePath/revalidateTag mark the cache stale for the next request - the current response still returns stale data, so reach for cache: "no-store" when you need freshness right now.revalidatePath("/") refreshes only the root page; pass "layout" - revalidatePath("/dashboard", "layout") - to invalidate the layout plus every child page under it.unstable_cache(fn, keyParts, { tags, revalidate }) is the tool - but give each call distinct, descriptive keyParts or you will return corrupted data from key collisions.router.refresh() (or tune experimental.staleTimes) to clear it.redirect() throws a sentinel error that Next.js catches to perform navigation; wrapping it in try/catch swallows the redirect, so place it after your mutation and outside any error handler.(prevState, formData) => Promise<State> and wire it through useActionState so pending state and error messages flow through React cleanly.addOptimistic inside startTransition - startTransition(async () => { addOptimistic(newItem); await saveItem(newItem) }) - so users see the change instantly and the pending flag still drives disabled states.<Suspense> so widgets stream as they resolve; one top-level boundary blocks the whole page on the slowest query and defeats streaming.error.tsx or <ErrorBoundary>) at the same level.useSearchParams() throws during static prerender, so the component reading it must sit inside a <Suspense> boundary or the build will fail.router.push; use router.replace so the back button returns to a meaningful previous page and also reset page=1 whenever the query changes.const token = (await cookies()).get("session")?.value - and touching either makes the route dynamic; read them in a small leaf component under <Suspense> when you want the rest of the route to stay static.cookies() is read-only in Server Components, so writes must happen in a Server Action, Route Handler, or Middleware - (await cookies()).set("session", token, { httpOnly: true, secure: true, sameSite: "lax" }).async function* plus for await...of to keep only one batch in memory, compose with map/filter/take helpers, and remember generators are server-only and single-use.Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥