Foundations — Cheat Sheet
Next.js · 7 topics. Download the PDF or the Instagram carousel and share it.
Rendering Models — CSR, SSR, SSG, ISR and RSC
Five ways to turn a component into HTML, differing in where and when it runs. Choosing per route rather than per application is the thing Next.js actually gives you.
- ✓Next.js lets each route choose its rendering model instead of committing the whole app to one
- ✓Static plus ISR gives CDN speed with controlled staleness; dynamic rendering costs a server round trip per visit
- ✓Reading cookies, headers or searchParams opts a route into dynamic rendering automatically
- ✓Server components run only on the server: they can await data directly and their code never reaches the browser
- ✓Server components hold data and structure; anything with state, effects or event handlers must be a client component
// CSR — Client-Side Rendering // The server sends an empty div. React builds the page in the browser. // + cheap to host, great for logged-in app screens // - blank until JS loads, weak for SEO, slow on a mid-range phone // SSG — Static Site Generation (build time) // HTML produced once, at deploy, served from a CDN. // + fastest possible, cheapest, perfect SEO // - stale until the next deploy; not for per-user content // Next: the default for a route with no dynamic data. // ISR — Incremental Static Regeneration // Static, but regenerated in the background every N seconds. // + CDN speed with fresh-enough content // - a user can see a stale copy for up to N seconds export const revalidate = 3600 // SSR — Server-Side Rendering (per request) // HTML built on the server for each request. // + always fresh, personalised, SEO-friendly // - a server round trip on every visit; you pay for compute // RSC — React Server Components (App Router) // Components that run ONLY on the server and stream their output. // Their code — and the libraries they import — never reach the browser. // This is the default in the App Router, not an opt-in.
The App Router — Files Are the Routing
A folder is a URL segment and a handful of reserved filenames give you the page, its layout, its loading state and its error boundary. There is no route configuration to write.
- ✓Folders are URL segments and reserved filenames declare what each segment renders — there is no route config
- ✓Only page.tsx or route.ts makes a segment routable, so components can safely be colocated inside route folders
- ✓Reading searchParams forces a route to be dynamic; useSearchParams in a small client component keeps the rest static
- ✓generateStaticParams pre-renders known dynamic paths at build time
- ✓Route groups organise without changing the URL, and _folders are never routable
app/
layout.tsx // root layout — REQUIRED, wraps everything
page.tsx // /
loading.tsx // Suspense fallback for this segment
error.tsx // error boundary ('use client', required)
not-found.tsx // 404 for this segment
global-error.tsx // catches errors in the root layout itself
problems/
page.tsx // /problems
loading.tsx // shown while /problems streams
[slug]/
page.tsx // /problems/two-sum
opengraph-image.tsx
api/
health/route.ts // GET /api/health — a handler, not a page
// Only page.tsx and route.ts make a segment publicly routable. A
// folder of components with no page.tsx is not a URL — which is why
// colocating components inside a route folder is safe.
// Dynamic segments
[slug] // /problems/two-sum -> params.slug
[...path] // catch-all -> params.path is an array
[[...path]] // optional catch-all -> also matches the parentServer and Client Components
Everything is a server component until a file says otherwise. Knowing exactly what "use client" does — and what it does not — is the single most important thing in the App Router.
- ✓"use client" marks a bundle boundary, not a runtime — client components still render on the server for the initial HTML
- ✓The directive is inherited through imports, so one at the top of a tree pulls the whole tree into the bundle
- ✓Push "use client" down to the leaves, and pass server-rendered content through as children
- ✓Props crossing into a client component must be serializable — functions and class instances cannot cross
- ✓Those props are embedded in the HTML, so select fields explicitly and use the server-only package to guard server modules
'use client' // at the TOP of a file, before imports // It means: this module and every module it imports goes into the // client bundle. It does NOT mean "does not run on the server" — // a client component still renders on the server for the initial // HTML, then hydrates in the browser. // The boundary is inherited DOWNWARD through imports: // ClientThing.tsx has 'use client' // -> everything it imports is client too, even without the directive // So one 'use client' at the top of a tree pulls the whole tree in. // Server components cannot: // useState useEffect useRef useContext // onClick and any event handler // window document localStorage // any browser-only library // Client components cannot: // be async / await data directly // import server-only code (a database client, a secret) // read the filesystem // The error that teaches this: // "You're importing a component that needs useState. It only works // in a Client Component, but none of its parents are marked with // 'use client'."
Navigation — Link, useRouter and the Router Cache
Link prefetches on hover and swaps only the part of the tree that changed. The surprises come from the client-side cache sitting between you and fresh data.
- ✓Link prefetches in the viewport and swaps only the changed segment, keeping layouts mounted
- ✓Import useRouter from next/navigation — next/router is the Pages Router and will not work
- ✓The client router cache is the usual cause of stale data after a save; router.refresh or revalidatePath clears it
- ✓Next 15 sets the dynamic router cache to 0 by default, which removed most staleness complaints
- ✓Update filters with router.replace and scroll: false so the page neither stacks history nor jumps to the top
import Link from 'next/link'
<Link href="/problems">Problems</Link>
<Link href={`/problems/${slug}`} prefetch={false}>Open</Link>
<Link href="/dashboard" replace>Go</Link> // no history entry
// Link prefetches when it enters the viewport (production only), so
// the destination is often already loaded before the click. That is
// most of why App Router navigation feels instant.
// Programmatic — note the import path
'use client'
import { useRouter } from 'next/navigation' // NOT next/router
const router = useRouter()
router.push('/problems/two-sum')
router.replace('/login')
router.back()
router.refresh() // re-fetch the current route from the server
// Reading the current location, all from next/navigation:
const pathname = usePathname() // '/problems/two-sum'
const params = useParams() // { slug: 'two-sum' }
const search = useSearchParams() // read-only
// useSearchParams makes a client component dynamic, so wrap it in a
// Suspense boundary or the whole route opts out of static rendering.Layouts, Loading States and Error Boundaries
A layout persists across navigation while its children change. loading.tsx and error.tsx are Suspense and error boundaries you get by naming a file.
- ✓Layouts persist across navigation between their children, so their client state and scroll position survive
- ✓Because a layout does not re-run on those navigations, per-page data belongs in the page, not the layout
- ✓loading.tsx is a Suspense fallback for the segment; finer Suspense boundaries inside a page stream each slow part separately
- ✓error.tsx must be a client component, contains failures to its segment, and receives a reset function
- ✓notFound() and redirect() work by throwing, so calling them inside a try/catch swallows them
// app/layout.tsx — the root. Required, and must render html and body.
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Header />
{children}
<Footer />
</body>
</html>
)
}
// app/(app)/dashboard/layout.tsx — nests inside the root
export default function DashboardLayout({ children }) {
return <div className="grid grid-cols-[240px_1fr]"><Sidebar />{children}</div>
}
// Navigating /dashboard -> /dashboard/billing re-renders only the
// page. The sidebar is NOT re-rendered, so its scroll position, its
// open sections and any client state inside it survive.
// The corollary that surprises people: a layout does not re-run on
// navigation between its children, so data fetched in a layout is
// NOT refreshed by moving between them. Fetch per-page data in the
// page.
// template.tsx is the escape hatch — same shape, but a fresh instance
// on every navigation. Use it when you want an enter animation or
// state reset per route.Project Structure and TypeScript Conventions
The app directory is routing, not architecture. Deciding what belongs beside a route and what belongs in a shared layer is what keeps a Next codebase navigable past twenty routes.
- ✓The app directory describes URLs; domain logic and shared components belong outside it
- ✓Underscore-prefixed folders are never routable, which makes colocating route-specific code safe
- ✓server-only and client-only turn a wrong import into a build failure instead of a leak
- ✓typedRoutes makes a mistyped href a compile error, and Next 15 params must be awaited
- ✓Settle route naming, a per-feature queries file and a per-feature actions file before the codebase grows
src/
app/
layout.tsx
page.tsx
problems/
page.tsx
loading.tsx
_components/ProblemCard.tsx // used by this route only
_lib/format.ts
(auth)/
login/page.tsx
register/page.tsx
api/
webhooks/razorpay/route.ts
components/
ui/Button.tsx // design-system primitives
shared/EmptyState.tsx
lib/
api-client.ts // transport
db.ts // server-only
auth.ts
features/
problems/ // domain logic used by several routes
queries.ts
actions.ts
types.ts
// The rule: app/ describes URLs. Anything that is not about a URL —
// a domain model, a query, a shared component — belongs outside it.
// _folders are never routable, which is what makes colocation safe.
// Prefix server-only modules and enforce it:
import 'server-only' // in lib/db.ts — a client import fails the build
import 'client-only' // in a browser-API moduleHydration Errors and the Usual Traps
The server rendered one thing and the browser expected another. Hydration mismatches have a short list of causes, and knowing it turns a baffling error into a two-minute fix.
- ✓A hydration error means the server HTML and the client render disagree for the same props
- ✓The causes are dates, randomness, browser APIs read during render, invalid HTML nesting, and browser extensions
- ✓Prefer rendering after mount; suppressHydrationWarning covers one element and ssr: false is a last resort
- ✓Format dates and numbers with an explicit locale or the build machine and the user will disagree
- ✓Dev renders per request and skips caching — always verify with a production build before trusting a fix
// 1. A value that differs between server and client
new Date().toLocaleString() // server timezone vs the user's
Math.random()
Date.now()
// Fix: render it in an effect, or pass a fixed value down.
// 2. A browser API read during render
if (window.innerWidth < 768) // ReferenceError on the server
localStorage.getItem('theme') // undefined on the server
// Fix: read it in useEffect, and accept one frame of the default.
// 3. Invalid HTML nesting — the browser repairs it, React does not
<p><div>…</div></p> // the browser closes the p early
<a><a>…</a></a>
<table><div/></table>
// This one is easy to miss, because the error points at hydration
// rather than at the markup.
// 4. Extensions that mutate the DOM before hydration
// Password managers and translators add attributes to inputs.
// Test in an incognito window before chasing your own code.
// 5. Locale or currency formatting without a fixed locale
new Intl.NumberFormat().format(n) // varies by machine
new Intl.NumberFormat('en-IN').format(n) // deterministic