React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

best-practicessummarynextjs-data

Next.js Data Best Practices

A condensed summary of the 25 most important best practices drawn from every page in this section.

  1. Fetch Inside async Server Components: Put 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.
  2. Await params and searchParams: In Next.js 15+, both are Promises, so destructuring without await yields a Promise object (often rendered as [object Promise]); always do const { slug } = await params.
  3. Parallelize Independent Fetches With Promise.all: Wrap independent reads — 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.
  4. Avoid Accidental Waterfalls: Never chain 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.
  5. Use Promise.allSettled for Optional Data: When one source is non-critical (recommendations, analytics), Promise.allSettled prevents a single rejection from discarding the other successful results and lets you render a partial page.
  6. Wrap Non-fetch Calls With React.cache: Request memoization only covers the 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.
  7. Isolate no-store Fetches Behind Suspense: A single 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.
  8. Be Explicit About fetch Cache Options: The Next.js 15 default is cache: "auto" (not force-cache), so set cache or next.revalidate on every fetch rather than relying on whatever the framework infers.
  9. Name Revalidation Tags Specifically: Tags are global — fetch(url, { next: { tags: ["product-abc123"] } }) — use namespaced strings instead of generic "data" so revalidateTag("product-abc123") does not quietly blow away unrelated caches.
  10. Understand Stale-While-Revalidate: 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.
  11. Use "layout" to Refresh Nested Pages: revalidatePath("/") refreshes only the root page; pass "layout"revalidatePath("/dashboard", "layout") — to invalidate the layout plus every child page under it.
  12. Use unstable_cache With Unique Keys: For non-fetch sources that need persistent caching, 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.
  13. Call router.refresh After Client Mutations: After a Server Action updates data, the client-side Router Cache can keep serving stale pages on back/forward, so trigger router.refresh() (or tune experimental.staleTimes) to clear it.
  14. Validate Server Action Inputs on the Server: Server Actions are reachable POST endpoints whose arguments may be closed-over values shipped to the client, so validate every field inside the action and never trust closures to hide secrets.
  15. Call redirect Outside try/catch: 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.
  16. Return Errors via useActionState: When a form needs validation feedback, type the action as (prevState, formData) => Promise<State> and wire it through useActionState so pending state and error messages flow through React cleanly.
  17. Pair useOptimistic With useTransition: For snappy UIs, call addOptimistic inside startTransitionstartTransition(async () => { addOptimistic(newItem); await saveItem(newItem) }) — so users see the change instantly and the pending flag still drives disabled states.
  18. Use Granular Suspense Boundaries: Wrap each independent data source in its own <Suspense> so widgets stream as they resolve; one top-level boundary blocks the whole page on the slowest query and defeats streaming.
  19. Size Skeletons to Match Final Content: Fallbacks must share the aspect ratio or fixed dimensions of the real content, otherwise the layout jumps when data arrives and you take an avoidable CLS hit.
  20. Pair Suspense With error.tsx: If any async child throws, the error bubbles past Suspense; every Suspense boundary should have a matching error boundary (error.tsx or <ErrorBoundary>) at the same level.
  21. Wrap useSearchParams in Suspense: On the client, useSearchParams() throws during static prerender, so the component reading it must sit inside a <Suspense> boundary or the build will fail.
  22. Prefer router.replace for Filter Updates: Filters, sort, and search typing create noisy history entries with router.push; use router.replace so the back button returns to a meaningful previous page and also reset page=1 whenever the query changes.
  23. Await cookies() and headers(): Both are async in Next.js 15+ — 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.
  24. Set Cookies Only in Actions or Middleware: 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" }).
  25. Stream Large Datasets With Async Generators: For paginated APIs, CSV exports, or SSE endpoints, use 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.