Cheat SheetsNext.jsData & Caching

Data & Caching — Cheat Sheet

Next.js · 8 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Data & Caching
Next.js8 topicsQuick revision reference
1

Fetching Data in Server Components

An async component that awaits its own data. No useEffect, no loading flag, no API route in the middle — and the waterfall problem moves from the browser to your control.

  • A server component can be async and await its data, removing the loading flag, error state and race handling entirely
  • Errors thrown during fetching are caught by error.tsx, so there is no error state to manage
  • Independent awaits must be wrapped in Promise.all, or each one waits for the last
  • Nested async components serialise; give each a Suspense boundary so they stream in parallel
  • React memoizes identical fetch calls within one render, and cache() extends that to database and SDK calls
async component, await, throw
// app/problems/page.tsx — a server component
export default async function ProblemsPage() {
  const problems = await getProblems()        // no useEffect, no state
  return <ProblemList problems={problems} />
}

// It can talk to the database directly, because it runs on the server
import { db } from '@/lib/db'
const problems = await db.problem.findMany({ where: { published: true } })

// Or to your own API — for this platform, FastAPI on Fly:
const res = await fetch(`${process.env.API_URL}/problems`, {
  headers: { Authorization: `Bearer ${process.env.SERVICE_TOKEN}` },
})
if (!res.ok) throw new Error(`API ${res.status}`)   // -> error.tsx
const problems = await res.json()

// A thrown error goes to the nearest error.tsx. A notFound() goes to
// not-found.tsx. There is no error STATE to manage — the boundaries
// are the error handling.

// Note what is NOT needed here: an /api route in your own app just to
// reach your own database. That indirection exists in the Pages
// Router because the browser was doing the fetching; here it is pure
// overhead.
2

The Caching Layers

Four caches sit between a request and your data, each with its own lifetime and its own way of being wrong. Naming them is what makes a staleness bug diagnosable.

  • Four caches: request memoization, the data cache, the full route cache and the client router cache
  • A hard refresh distinguishes them — fresh after a hard refresh means the client router cache is at fault
  • Reading cookies, headers or searchParams anywhere in a tree makes the whole route dynamic
  • The build output labels each route static or dynamic, which is how you catch an accidental regression
  • Next 14 caches fetch by default and Next 15 does not — always set cache or revalidate explicitly
Memoization, data, full route, router
// 1. REQUEST MEMOIZATION — React, per render pass, server
//    Dedupes identical fetch/cache() calls in one render.
//    Lifetime: one render. Not configurable, and never a bug.

// 2. DATA CACHE — Next, persistent, server, across requests AND deploys
//    Stores the RESULT of a fetch.
//    Next 14: cached by default.  Next 15: NOT cached by default.
fetch(url, { cache: 'force-cache' })          // opt in (15)
fetch(url, { cache: 'no-store' })             // opt out (14)
fetch(url, { next: { revalidate: 60 } })      // time-based
fetch(url, { next: { tags: ['problems'] } })  // taggable

// 3. FULL ROUTE CACHE — Next, build/server, the rendered HTML and RSC
//    payload of a STATIC route. Cleared by a deploy or revalidation.

// 4. ROUTER CACHE — the browser, in memory, per session
//    Rendered segments from routes you visited.
//    Next 14: 30s dynamic / 5min static.  Next 15: 0 / 5min.

// Reading order on a request:
//   router cache -> full route cache -> data cache -> your source

// Diagnostic that saves an hour: does a HARD REFRESH show fresh data?
//   yes -> the router cache (client)
//   no  -> the data cache or the full route cache (server)
3

Revalidation — Keeping Cached Data Fresh

Time-based revalidation is a guess about how stale you can afford to be. On-demand revalidation is knowing exactly when something changed — and it is almost always the better answer.

  • Time-based revalidation serves a stale copy to the first visitor after expiry and regenerates in the background
  • A route revalidates at the shortest interval of anything inside it
  • On-demand revalidation with tags invalidates exactly the pages a write affected and is the better default
  • An external revalidation endpoint must be authenticated, or anyone can force continuous regeneration
  • Never cache per-user data — a cache keyed by URL alone will eventually serve one user another user's page
Stale-while-revalidate, shortest interval wins
// Whole route
export const revalidate = 3600            // seconds

// One fetch
fetch(url, { next: { revalidate: 60 } })

// The route uses the SHORTEST revalidate of anything inside it.

// The behaviour is stale-while-revalidate, which surprises people:
//   request at t=0      -> generated, cached
//   request at t=30     -> served from cache (fast)
//   request at t=70     -> STALE COPY SERVED, regeneration starts
//   request at t=71     -> fresh copy
// So the first visitor after expiry still sees the old page. Nobody
// waits for a rebuild, and nobody is guaranteed the newest content.

// Choose the interval from how wrong you can afford to be:
//   a published article        3600 or more
//   a problem list             300
//   a leaderboard              30, or on-demand
//   a price, a balance         never cache

// revalidate = 0 means "always dynamic" and is the same as
// force-dynamic — not "revalidate immediately".
4

Streaming and Suspense

Send the shell immediately and let slow parts arrive as they resolve. One slow query stops holding the entire page hostage.

  • Streaming sends the shell immediately and pushes slow content down the same response as it resolves
  • The await must be inside the Suspense boundary, or the page waits before rendering anything
  • Start fetches without awaiting and pass the promises down to avoid a waterfall between boundaries
  • use() lets a client component consume a promise started on the server
  • Once streaming starts the status and headers are sent, so auth checks and redirects must happen before the first boundary
Put the await inside the boundary, not before it
// No boundary — the whole page waits for the slowest query
export default async function Page() {
  const [problem, stats, comments] = await Promise.all([...])   // 900ms
  return <>…</>                                  // nothing until then
}

// Boundaries — the shell is instant, each part streams
export default async function Page({ params }) {
  const problem = await getProblem(params.slug)   // fast, 40ms
  return (
    <>
      <ProblemHeader problem={problem} />         {/* immediate */}

      <Suspense fallback={<StatsSkeleton />}>
        <Stats slug={params.slug} />              {/* 300ms, streams */}
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <Comments slug={params.slug} />           {/* 900ms, streams */}
      </Suspense>
    </>
  )
}

// The awaited component must be INSIDE the boundary. This does not
// stream — the await happens before any JSX is returned:
const comments = await getComments()
return <Suspense fallback={...}><List items={comments} /></Suspense>

// loading.tsx is the same mechanism at segment level: it wraps the
// whole page in one boundary.
5

Static or Dynamic — Reading the Build Output

Every route is one or the other, the build tells you which, and a route silently flipping to dynamic is the most common performance regression in a Next codebase.

  • The build output labels every route static, SSG, dynamic or edge — read it after each build
  • A route that unexpectedly became dynamic is the most common silent performance regression
  • Dynamic APIs are inherited downward, so one cookies() call in a root layout makes the whole app dynamic
  • Keep a page static by moving per-user UI into a client component or isolating it behind Suspense
  • generateStaticParams with dynamicParams: true prerenders the popular paths and renders the tail on demand
Read this after every build
Route (app)                              Size     First Load JS
┌ ○ /                                    5.2 kB          98 kB
├ ● /learn/[topic]/[concept]             1.8 kB          95 kB
├   ├ /learn/react/react-usestate
├   └ [+199 more paths]
├ ƒ /dashboard                           3.1 kB          96 kB
└ ƒ /api/health                          0 B                0 B

// ○ Static     prerendered at build, served from the CDN. Fastest.
// ● SSG        prerendered from generateStaticParams. Also CDN.
// ƒ Dynamic    rendered per request on a server. Costs latency and money.
// λ / Edge     runs on the edge runtime.

// What to look for after a build:
//   - a page you expected to be ○ showing as ƒ
//   - First Load JS creeping up release after release
//   - a route with far more paths than you expected

// Diffing this between deploys catches regressions that no test does.
// Some teams commit the summary and review changes to it.

// Force a decision when you want it enforced rather than inferred:
export const dynamic = 'force-static'    // build fails on dynamic API use
export const dynamic = 'force-dynamic'   // never prerender
6

Partial Prerendering

One route that is static and dynamic at once: the shell comes from the CDN instantly, and the personalised holes stream in. It removes the choice this whole category has been about.

  • PPR serves a prerendered shell from the edge and streams the request-dependent parts into it
  • It moves the static-versus-dynamic decision from the route level to the component level
  • Any dynamic read must sit inside a Suspense boundary, and the fallback is part of the static shell
  • The shell should carry the real content — a shell made only of skeletons gains nothing
  • It is still experimental, but the small-boundary discipline it requires improves streaming today
Static shell from the CDN, dynamic holes streamed
// next.config.js
module.exports = { experimental: { ppr: 'incremental' } }

// app/problems/[slug]/page.tsx
export const experimental_ppr = true

export default async function Page({ params }) {
  const problem = await getProblem(params.slug)     // static: build time

  return (
    <>
      <ProblemHeader problem={problem} />           {/* prerendered shell */}
      <ProblemBody problem={problem} />             {/* prerendered shell */}

      <Suspense fallback={<BookmarkSkeleton />}>
        <BookmarkState slug={params.slug} />        {/* reads cookies() —
                                                        streamed per request */}
      </Suspense>
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments slug={params.slug} />             {/* live data */}
      </Suspense>
    </>
  )
}

// What the user gets:
//   ~0ms   the shell, from the CDN — content is readable immediately
//   ~200ms the personalised holes fill in
// Before PPR this page had to be entirely dynamic because of one
// cookie read.
7

Client-Side Data Alongside Server Components

Server components do not remove the need for client fetching. Knowing which of the two a piece of data belongs to — and how to hand data from one to the other — is the real skill.

  • Server components own the first paint and SEO; client fetching owns interaction, polling and optimistic updates
  • Pass server-fetched data as initialData so the client cache does not refetch it on mount
  • HydrationBoundary transfers a whole prefetched cache from server to client
  • For a read-heavy app, server components plus server actions often remove the need for a data library entirely
  • Never fetch your own route handler from a server component — that is an HTTP round trip to yourself
First paint and SEO vs interaction and polling
// SERVER COMPONENT when the data is:
//   needed for the first paint or for SEO
//   the same for everyone, or cacheable per user
//   large, or expensive to compute
//   from a source with a secret (a database, a keyed API)

// CLIENT when the data:
//   changes in response to interaction (search-as-you-type, filters)
//   polls or streams (a live job status, a leaderboard)
//   is optimistically updated (a like, a toggle)
//   depends on a browser API (geolocation, media queries)

// The tell that you have it wrong: a server round trip on every
// keystroke, or a spinner on a page that could have been prerendered.

// A search page usually wants BOTH: the initial results rendered on
// the server for SEO and first paint, then client-side fetching as
// the user types.
8

Calling a Separate Backend

When the real API is FastAPI or Spring Boot, Next becomes a client of it — and the choice of whether the browser or the Next server makes that call changes auth, CORS and latency.

  • Calling the backend from the Next server removes CORS entirely and keeps the token off the browser, at the cost of a hop
  • A server component sends no cookies automatically — forward them explicitly or the request is anonymous
  • Per-user responses must use no-store, or one user's data can be served inside another user's page
  • Generate client types from the backend's OpenAPI schema and fail CI on drift
  • Bound every outbound call with a timeout and degrade gracefully, since two platforms mean two cold starts
Direct, server-side, or proxied
// A. The browser calls the backend directly
//    browser -> api.aicancode.org (FastAPI on Fly)
//    + one hop, no Next server involved
//    - CORS applies, the token must be reachable by JS or the cookie
//      must be same-site, and nothing is server-rendered
NEXT_PUBLIC_API_URL=https://api.aicancode.org

// B. The Next server calls the backend, then renders
//    browser -> Next (Vercel) -> api.aicancode.org
//    + no CORS at all (server to server), the token never reaches the
//      browser, HTML arrives ready to read, SEO works
//    - an extra hop, and Vercel and Fly should be in the same region
//      or you pay for the distance twice
API_URL=https://api.aicancode.org          // server-only, no NEXT_PUBLIC_

// Most apps use both: B for the initial render and anything secret,
// A for interactive updates after the page is live.

// A third option worth knowing — proxy through Next so the browser
// only ever sees one origin:
// next.config.js
async rewrites() {
  return [{ source: '/api/:path*', destination: `${process.env.API_URL}/:path*` }]
}
// Cookies become first-party and CORS disappears, at the cost of
// routing traffic through Vercel.
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/nextjs