1. Overview & Questions (SQ3R: Survey & Question)

SQ3R Step 1: Quickly survey the full picture, then formulate key questions.

What is Next.js?

Next.js is a full-stack React framework developed and maintained by Vercel. It is not merely a wrapper around a front-end library — it is a complete application development solution. From routing, rendering, and data fetching to deployment, it covers the entire lifecycle of web application development.

The motivation behind Next.js is straightforward: React by itself is just a UI library. To build a complete web application, developers must handle routing, server-side rendering, code splitting, data fetching, and more on their own. Next.js fills these gaps, enabling developers to build production-grade applications with React quickly.

The current latest version is Next.js 16 (documentation version 16.2.6), which introduces major features such as Cache Components and Partial Prerendering (PPR), representing a fundamental shift in the architecture model.

Key Questions

  • When is Next.js the right choice? — From static blogs to dynamic SaaS platforms, from content sites to e-commerce, Next.js handles them all.
  • What advantages does Next.js have over alternatives? — Built-in SSR/SSG/ISR, file-system routing, automatic code splitting, and native React Server Components support.
  • What prerequisites are needed? — A working knowledge of HTML, CSS, JavaScript, and React fundamentals.

Technology Landscape

The Next.js architecture can be visualized as a layered tree:

Next.js Application
├── App Router (/app)              ← Recommended routing system
│   ├── Layouts (layout.js)        ← Shared layouts, do not re-render on navigation
│   ├── Pages (page.js)            ← Route entry points, one per route
│   ├── Loading States (loading.js) ← Automatic Suspense boundaries
│   ├── Error Handling (error.js)  ← Error boundaries
│   ├── API Routes (route.js)      ← Server-side API endpoints
│   └── Special Files (not-found.js, etc.)
├── Server Components              ← Default, server-rendered, zero JS shipped to client
├── Client Components ("use client") ← Client-side interactivity, used when needed
├── Cache Components ("use cache")   ← New in Next.js 16, declarative caching
├── Data Fetching (fetch / ORM)     ← Direct await in Server Components
├── Server Actions ("use server")  ← Server functions for form submissions
└── next.config.ts                 ← Global configuration

2. Explain It Simply (Feynman Technique)

Feynman Technique core idea: If you cannot explain something in simple language, you do not truly understand it.

Core Concepts Explained

1. File-System Routing

Next.js routing is entirely determined by your folder structure. Create a folder inside app/, and it becomes a route. Place a page.js inside, and it becomes the page for that route.

// app/page.tsx → corresponds to route /
export default function HomePage() {
  return <h1>This is the home page</h1>;
}
 
// app/about/page.tsx → corresponds to route /about
export default function AboutPage() {
  return <h1>About Us</h1>;
}
 
// app/blog/[id]/page.tsx → corresponds to route /blog/123 (dynamic)
export default async function BlogPost({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return <h1>Post {id}</h1>;
}

No routing configuration files needed. The folder structure is the routing table.

2. Layouts

A layout is a shared wrapper that encloses all child pages. The key insight: layouts do not re-render on navigation.

// app/layout.tsx → Root layout (must include html and body tags)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <nav>Navigation (does not re-render on navigation)</nav>
        <main>{children}</main>
      </body>
    </html>
  );
}

3. Server Components and Client Components

This is the most fundamental concept in Next.js. By default, all components are Server Components — they run on the server and send zero JavaScript to the client. When you need interactivity (click handlers, state management, browser APIs), you use Client Components.

// Server Component (default) - runs on server, sends HTML to client
// app/page.tsx
import { db } from "@/lib/db";
import LikeButton from "./like-button";
 
export default async function Page() {
  const posts = await db.query("SELECT * FROM posts"); // Direct database access
  return (
    <div>
      {posts.map((post) => (
        <p key={post.id}>{post.title}</p>
      ))}
      <LikeButton /> {/* Interactive part handled by Client Component */}
    </div>
  );
}
// Client Component - add "use client" when interactivity is needed
// app/like-button.tsx
"use client";
 
import { useState } from "react";
 
export default function LikeButton() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Like {count}</button>;
}

4. Data Fetching

Data fetching in Server Components is remarkably simple — just use async/await.

// Direct fetch in Server Component
export default async function BlogPage() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();
 
  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

No useEffect, no useState, no client-side data fetching library required. The code reads like a plain async function.

5. Caching (New in Next.js 16)

Next.js 16 introduces Cache Components, making caching declarative via the "use cache" directive.

// Enable in next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
  cacheComponents: true,
};
export default nextConfig;
// Data-level caching
import { cacheLife } from "next/cache";
 
export async function getUsers() {
  "use cache";
  cacheLife("hours"); // Cache for one hour
  return db.query("SELECT * FROM users");
}
// UI-level caching
import { cacheLife } from "next/cache";
 
export default async function BlogPosts() {
  "use cache";
  cacheLife("hours");
  const posts = await fetch("https://api.example.com/posts");
  return <div>{/* Render post list */}</div>;
}

6. Server Actions

Server Actions let you define server-side functions directly in your components, eliminating the need for manual API routes.

// app/actions.ts
"use server";
 
import { revalidateTag } from "next/cache";
 
export async function createPost(formData: FormData) {
  await db.post.create({
    data: { title: formData.get("title") as string },
  });
  revalidateTag("posts"); // Invalidate cache
}
// Use in a form
import { createPost } from "./actions";
 
export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Post title" />
      <button type="submit">Publish</button>
    </form>
  );
}

Analogies and Metaphors

Think of a Next.js application as a restaurant:

  • Server Components = Kitchen chefs — they prepare dishes (HTML) behind the scenes. Diners never see the cooking process, only the finished plate. The benefit is that the kitchen can use heavy equipment (databases, file systems) that diners don't need at their tables.
  • Client Components = Condiment bottles on the table — things diners interact with themselves, like adding soy sauce (clicking buttons) or stirring (dragging).
  • Layout = Restaurant decor — no matter which dish you order (page navigation), the decor stays the same.
  • Cache Components ("use cache") = Pre-prepared dishes — cooked in advance, served instantly when ordered.
  • Streaming (Suspense) = Serving cold dishes first, then hot dishes — no need to wait for everything to be ready before serving.
  • Server Actions = Waiter ordering system — diners fill out a form, and the waiter automatically relays it to the kitchen.

Common Misconceptions Clarified

  1. Misconception: Next.js only does SSR — In reality, it supports SSG, SSR, ISR, CSR, and PPR (Partial Prerendering), and you can mix them within the same application.
  2. Misconception: All components need "use client" — The opposite is true. Everything is a Server Component by default; only add "use client" when interactivity is required.
  3. Misconception: Server Components cannot pass props to Client Components — They absolutely can, as long as the props are serializable (numbers, strings, plain objects, etc.).
  4. Misconception: Next.js 16 still uses Pages Router's getServerSideProps — The App Router is entirely different. Data fetching is a direct await, no special functions needed.
  5. Misconception: Caching is hard to control — Next.js 16's "use cache" + cacheLife + cacheTag makes caching declarative and manageable.

3. Cone of Depth (Simon Learning Method)

Simon Learning Method: Focused effort, goal-oriented, cone-shaped depth — start from the core and progressively expand outward.

Layer 1: Core Fundamentals

1. Creating a Project

npx create-next-app@latest

You will be prompted to select TypeScript, ESLint, Tailwind CSS, src/ directory, and App Router.

Manual installation:

npm install next@latest react@latest react-dom@latest
// package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  }
}

2. Project Structure

my-app/
├── app/
│   ├── layout.tsx          ← Root layout
│   ├── page.tsx            ← Home page (/)
│   ├── blog/
│   │   ├── page.tsx        ← /blog
│   │   └── [id]/
│   │       └── page.tsx    ← /blog/123
│   ├── about/
│   │   └── page.tsx        ← /about
│   └── api/
│       └── route.ts        ← API endpoint
├── public/                 ← Static assets
├── next.config.ts          ← Configuration
├── package.json
└── tsconfig.json

3. Pages and Layouts

// app/layout.tsx - Root layout (required)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
// app/blog/layout.tsx - Nested layout
export default function BlogLayout({ children }: { children: React.ReactNode }) {
  return (
    <section>
      <h2>Blog</h2>
      {children}
    </section>
  );
}

4. Linking and Navigation

import Link from "next/link";
 
export default function NavBar() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/blog/123">Post</Link>
    </nav>
  );
}

The <Link> component automatically prefetches pages for instant navigation.

5. Dynamic Routes

// app/blog/[id]/page.tsx
export default async function BlogPost({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return <h1>Post ID: {id}</h1>;
}

Catch-all routes: [...slug] matches multiple path segments, [[...slug]] is an optional catch-all.

6. Loading States

// app/blog/loading.tsx - Automatic Suspense boundary
export default function Loading() {
  return <div>Loading...</div>;
}

While blog/page.tsx fetches data on the server, users see "Loading..." immediately. The content is automatically swapped in once ready.

7. Error Handling

// app/error.tsx - Must be a Client Component
"use client";
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

8. API Routes (Route Handlers)

// app/api/posts/route.ts
import { NextResponse } from "next/server";
 
export async function GET() {
  const posts = await db.query("SELECT * FROM posts");
  return NextResponse.json(posts);
}
 
export async function POST(request: Request) {
  const body = await request.json();
  const post = await db.post.create({ data: body });
  return NextResponse.json(post, { status: 201 });
}

Layer 2: Advanced Usage

1. Composing Server and Client Components

The most common pattern: Server Components fetch data and pass it via props to Client Components for interactivity.

// app/page.tsx (Server Component)
import LikeButton from "./like-button";
import { getPost } from "@/lib/data";
 
export default async function Page() {
  const post = await getPost("1");
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <LikeButton likes={post.likes} />
    </article>
  );
}

You can also pass Server Components as children to Client Components:

// app/ui/modal.tsx (Client Component)
"use client";
 
export default function Modal({ children }: { children: React.ReactNode }) {
  return <div className="modal">{children}</div>;
}
// app/page.tsx (Server Component)
import Modal from "./ui/modal";
import Cart from "./ui/cart"; // Cart is a Server Component
 
export default function Page() {
  return (
    <Modal>
      <Cart /> {/* Server Component passed as children to Client Component */}
    </Modal>
  );
}

2. Context Providers

React Context is not supported in Server Components. Create a Provider in a Client Component:

// app/theme-provider.tsx
"use client";
 
import { createContext } from "react";
 
export const ThemeContext = createContext("light");
 
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
// app/layout.tsx
import ThemeProvider from "./theme-provider";
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

3. Streaming and Suspense

Suspense is central to streaming in Next.js. It lets you break a page into independently loading chunks:

import { Suspense } from "react";
 
export default function Page() {
  return (
    <>
      <h1>My Blog</h1>
      {/* This part displays immediately */}
      <p>Welcome to my blog</p>
      {/* This part streams in when data is ready */}
      <Suspense fallback={<div>Loading posts...</div>}>
        <BlogPosts />
      </Suspense>
      <Suspense fallback={<div>Loading comments...</div>}>
        <Comments />
      </Suspense>
    </>
  );
}

4. Data Fetching Patterns

Parallel fetching (recommended):

export default async function Page() {
  const artistPromise = getArtist();
  const albumsPromise = getAlbums();
 
  const [artist, albums] = await Promise.all([artistPromise, albumsPromise]);
 
  return (
    <div>
      <h1>{artist.name}</h1>
      <AlbumList albums={albums} />
    </div>
  );
}

Sequential fetching (when requests have dependencies):

export default async function Page() {
  const artist = await getArtist(); // Fetch artist first
  const playlists = await getPlaylists(artist.id); // Then use artist.id
  return <div>{/* ... */}</div>;
}

5. Caching and Revalidation (Next.js 16)

The Next.js 16 caching system is built around the "use cache" directive:

import { cacheLife, cacheTag } from "next/cache";
 
// Data-level caching with tags
export async function getProducts() {
  "use cache";
  cacheLife("hours"); // Cache lifetime
  cacheTag("products"); // Cache tag for on-demand revalidation
  return db.query("SELECT * FROM products");
}

Time-based revalidation:

"use cache";
cacheLife("seconds"); // Revalidate every second
cacheLife("minutes"); // Revalidate every minute
cacheLife("hours"); // Revalidate every hour
cacheLife("days"); // Revalidate every day
cacheLife("weeks"); // Revalidate every week
cacheLife("max"); // Maximum cache duration

On-demand revalidation:

// app/actions.ts
"use server";
import { revalidateTag, revalidatePath, updateTag } from "next/cache";
 
export async function refreshProducts() {
  revalidateTag("products"); // Revalidate all caches tagged 'products'
}
 
export async function refreshPage() {
  revalidatePath("/products"); // Revalidate all caches for /products path
}
 
export async function updateProducts() {
  updateTag("products"); // Immediately expire and regenerate
}

6. Runtime APIs with Caching

Runtime APIs (cookies(), headers(), searchParams) require per-request information. In Next.js 16, components using them must be wrapped in <Suspense>:

import { cookies } from "next/headers";
import { Suspense } from "react";
 
async function UserGreeting() {
  const cookieStore = await cookies();
  const theme = cookieStore.get("theme")?.value || "light";
  return <p>Your theme: {theme}</p>;
}
 
export default function Page() {
  return (
    <>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Loading...</p>}>
        <UserGreeting />
      </Suspense>
    </>
  );
}

7. Metadata and SEO

// app/layout.tsx - Static metadata
export const metadata = {
  title: "My Website",
  description: "This is my website description",
};
 
// app/blog/[id]/page.tsx - Dynamic metadata
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const post = await getPost(id);
  return {
    title: post.title,
    description: post.excerpt,
  };
}

Layer 3: Deep Dive

1. Rendering Architecture: Partial Prerendering (PPR)

The default rendering mode in Next.js 16 is Partial Prerendering. At build time, Next.js prerenders the entire component tree and decides how to handle each component based on the APIs it uses:

  • Components with "use cache" → results are cached and included in the static shell
  • Components wrapped in <Suspense> → fallback is included in the static shell, content streams at request time
  • Pure deterministic operations → automatically included in the static shell

This means users see the static page frame instantly (navigation, layout, cached content), and personalized content loads progressively via streaming.

2. RSC Payload

When Server Components render, they produce a special binary format called the RSC Payload. It contains:

  • The rendered output of Server Components
  • Placeholders for Client Components and references to their JavaScript files
  • Props passed from Server Components to Client Components

On first load, HTML provides an instant preview, the RSC Payload reconciles the component tree, and JavaScript hydrates Client Components. Subsequent navigations use the RSC Payload directly, without re-fetching HTML.

3. Environment Isolation and Security

Next.js enforces a strict security boundary between server and client:

  • Only environment variables prefixed with NEXT_PUBLIC_ are exposed to the client
  • Even if server-only code is accidentally imported on the client, sensitive variables are replaced with empty strings
  • The server-only package throws a build-time error if server code is imported into a client module
  • The client-only package prevents server modules from importing client code
import "server-only"; // Prevents client-side imports
 
export async function getData() {
  const res = await fetch("https://api.example.com/data", {
    headers: { authorization: process.env.API_KEY },
  });
  return res.json();
}

4. Best Practices for Minimizing Client JS

// Correct approach: Add "use client" only to the interactive component
// app/layout.tsx - This is a Server Component
import Search from "./search"; // Client Component
import Logo from "./logo"; // Server Component
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <Search /> {/* Only the search bar is a Client Component */}
      </nav>
      <main>{children}</main>
    </>
  );
}

Push "use client" as far down the component tree as possible to minimize the client JavaScript bundle.

5. Sharing Data: React.cache + Context

Share data across multiple components without duplicate requests:

import { cache, createContext } from "react";
 
// Create a cached function — multiple calls within the same request execute only once
const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } });
});

React.cache is scoped to the current request only. Each request gets its own memoization scope with no sharing between requests.

6. Third-Party Component Integration

For third-party components that lack the "use client" directive, create a wrapper file:

// app/carousel.tsx
"use client";
import { Carousel } from "acme-carousel";
export default Carousel;

Then import the wrapped component directly in your Server Component.

7. Route Segment Config

// app/api/route.ts
export const dynamic = "force-dynamic"; // Force dynamic rendering
export const revalidate = 0; // Disable caching
export const runtime = "edge"; // Use Edge Runtime
export const maxDuration = 60; // Max execution time in seconds

4. Key Notes (Cornell Note-Taking Method)

Cornell Method: Divide notes into cue column, note column, and summary column for easy review and retrieval.

Key Concept Quick Reference

Cue / KeywordDetailed Notes
App RouterRouting system based on /app directory, supports nested layouts and Server Components
Server ComponentsDefault component type, server-rendered, ships no JS to client, can directly access databases
Client ComponentsAdd "use client" directive, used for interactivity, state management, browser APIs
Cache ComponentsNew in Next.js 16, "use cache" directive for declarative caching
PPR (Partial Prerendering)Default rendering mode, static shell + streaming dynamic content
Server ActionsServer functions defined with "use server", can be bound directly to form actions
RSC PayloadBinary format produced by Server Components rendering, used for client reconciliation
StreamingFlow-based rendering via <Suspense>, show fallback first then stream in content
cacheLifeUsed with "use cache" to set cache duration (seconds/minutes/hours etc.)
cacheTagTags for caches, combined with revalidateTag / updateTag for on-demand revalidation
File-System RoutingFolder structure defines routes, page.js for pages, layout.js for layouts
generateStaticParamsPre-generate static parameters for dynamic routes, used for SSG

Core API Quick Reference

API / ConfigPurposeExample
"use client"Declare Client Component'use client' at top of file
"use server"Declare Server Action'use server' at top of function or file
"use cache"Declare cache (requires cacheComponents enabled)'use cache' at top of function/component
cacheLife(profile)Set cache policycacheLife('hours')
cacheTag(tag)Tag a cache entrycacheTag('products')
revalidateTag(tag)Revalidate cache by tagrevalidateTag('products')
revalidatePath(path)Revalidate cache by pathrevalidatePath('/blog')
updateTag(tag)Immediately expire and regenerate cacheupdateTag('posts')
cookies()Read request cookiesconst store = await cookies()
headers()Read request headersconst h = await headers()
redirect(path)Redirectredirect('/login')
notFound()Trigger 404 pagenotFound()
<Link href>Client-side navigation with prefetching<Link href="/about">About</Link>
<Image>Image optimization<Image src="/photo.jpg" alt="..." width={500} height={300} />
<Suspense>Streaming render boundary<Suspense fallback={<Loading />}><Component /></Suspense>
generateMetadataDynamically generate page metadataexport async function generateMetadata() { ... }
generateStaticParamsPre-generate dynamic route paramsexport async function generateStaticParams() { ... }
connection()Mark code requiring per-request dataawait connection() before Date.now() etc.

Section Summary

Next.js 16 is a full-stack React framework centered around the App Router. It defaults to Server Components for zero-JS server rendering and introduces Client Components via "use client" for interactivity. Next.js 16's Cache Components ("use cache") and Partial Prerendering radically simplify caching strategy, unifying the rendering model into "static shell + streaming dynamic content." Data fetching is a straightforward async/await in Server Components, eliminating the need for client-side data libraries. Server Actions via "use server" bring simplicity back to form submissions and mutations.


5. Review & Practice (SQ3R: Recite & Review)

SQ3R final steps: Recite key points and solidify understanding through practice.

Key Points Recap

  1. File-system routing: The app/ directory structure is the routing table. page.js defines pages, layout.js defines layouts.
  2. Server Components are the default: No directive needed. Components render on the server and ship no JS to the client.
  3. "use client" is a boundary: It marks the dividing line between server and client. The file and all its imports are bundled for the client.
  4. Layouts do not re-render: layout.js persists across navigation; only page.js content updates.
  5. Data fetching uses async/await: Server Components directly await fetch calls or ORM queries. No useEffect needed.
  6. "use cache" + cacheLife: Next.js 16's declarative caching solution, replacing previous fetch cache options.
  7. Streaming is a core pattern: Wrap async content in <Suspense>, show a fallback, then stream in the resolved content.
  8. Server Actions simplify mutations: "use server" defines server functions that bind directly to form action props.
  9. PPR is the default rendering mode: Static content is prerendered, dynamic content streams in, combining the best of both worlds.
  10. Environment security: Non-NEXT_PUBLIC_ environment variables are never exposed to the client. The server-only package provides additional protection.

Hands-On Exercises

Exercise 1: Personal Blog (Beginner)

Build a simple blog application:

  • Home page displaying a list of posts
  • Post detail page using dynamic routing [slug]
  • Add loading.tsx and error.tsx
  • Use generateStaticParams to pre-generate pages

Exercise 2: Cached Product Catalog (Intermediate)

Build a product showcase page:

  • Enable cacheComponents: true
  • Use "use cache" + cacheLife('hours') to cache product data
  • Use cacheTag('products') to tag cached entries
  • Create a Server Action that calls updateTag('products') to refresh the cache

Exercise 3: Full-Stack Dashboard (Advanced)

Create an authenticated dashboard:

  • Use cookies() to read session data
  • Wrap personalized content in <Suspense> for streaming
  • Server Components fetch data, Client Components handle chart interactions
  • Use a Context Provider to share theme state

Common Pitfalls

  1. Using useState or onClick in a Server Component — These are client-side APIs. Solution: Split into Server + Client Components and add "use client" to the interactive part.
  2. Directly accessing a database in a Client Component — Client Components run in the browser and cannot access server resources. Solution: Fetch data in a Server Component and pass it as props.
  3. Using cookies() in a layout without a Suspense boundary — Next.js 16 throws a build error for runtime API access in cached contexts. Solution: Extract runtime data access into a child component and wrap it in <Suspense>.
  4. Forgetting to await params — In Next.js 16, params is a Promise type. You must await it before accessing its properties.
  5. Overusing "use client" — Marking an entire page as a Client Component sends large amounts of JS to the client. Solution: Apply "use client" only at the smallest interactive component granularity.

Further Reading