01 · What Next.js Adds to React¶
React is a library for describing UI as a function of state. It deliberately stops there. It has no opinion about URLs, no way to fetch data before the page reaches the browser, no build pipeline and no server. Every real React application therefore sits inside something that supplies those pieces. Next.js is one such something — a framework that bundles routing, rendering, data loading, code splitting and a server into one set of conventions.
This lesson maps out exactly which problems Next.js takes over, so that later lessons can each zoom in on one of them.
The gaps a React-only app has to fill¶
Start from a single-page app built with a bundler such as Vite and nothing else:
| Concern | React alone | What you'd add yourself |
|---|---|---|
| Mapping URLs to screens | Nothing | A client router (React Router, TanStack Router) |
| First paint | Empty <div id="root"> until JS runs |
Server-side rendering setup, hydration wiring |
| Data loading | useEffect + fetch after mount |
A data library, loaders, a backend API |
| Mutations | fetch('/api/...') from handlers |
A separate API server, CORS, auth plumbing |
| SEO / social previews | One static index.html for all URLs |
Per-route <title>, meta tags, sitemap |
| Code splitting | Manual lazy() |
Route-based chunking |
| Images and fonts | Whatever you ship | Resizing, lazy loading, font subsetting |
| Production server | Static file host | Node server, caching headers, CDN rules |
None of these is impossible to do yourself. The cost is that every team assembles a slightly different stack, and the pieces don't know about each other — your router doesn't know which data a route needs, so it cannot start loading it before the code arrives.
What Next.js provides¶
Next.js answers each row with a convention:
- Routing from the file system.
app/blog/page.tsxis the/blogpage. Nested folders are nested routes;layout.tsxfiles wrap everything beneath them. - Rendering on the server by default. Components in
app/are React Server Components: they run on the server (or at build time) and send HTML plus a compact description of the UI to the browser. JavaScript is shipped only for components you mark with"use client". - Data fetching where the UI is. A Server Component can be
asyncandawaita database query orfetchcall directly. NouseEffect, no loading spinner for the initial content, no API layer needed just to read data. - Mutations without writing an API. A function marked
"use server"(a Server Action) can be passed to a<form action={...}>. Next.js creates the endpoint. - Static generation and caching. Pages that don't depend on the request are rendered once at build time and served as files. Pages that do can be rendered per request, or partly static and partly streamed.
- Built-in optimisations.
next/imageresizes and lazy-loads images,next/fontself-hosts fonts with no layout shift, and the Metadata API produces<head>tags. - A build tool and a server.
next buildcompiles everything (with Turbopack by default in version 16) andnext startruns a production Node.js server. The output can also be deployed to platforms that understand Next.js, or exported as static files when you don't need a server.
A concrete comparison¶
Here is a page that shows a list of products. In a client-only React app:
import { useEffect, useState } from "react";
type Product = { id: number; name: string };
export function Products() {
const [products, setProducts] = useState<Product[] | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/products")
.then((r) => (r.ok ? r.json() : Promise.reject(r.statusText)))
.then(setProducts)
.catch((e) => setError(String(e)));
}, []);
if (error) return <p>Failed: {error}</p>;
if (!products) return <p>Loading…</p>;
return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}
The browser downloads JavaScript, runs it, renders "Loading…", then asks a separate
API for data, then renders the list. You also need that /api/products server.
The Next.js App Router version:
import { db } from "@/db";
export default async function ProductsPage() {
const products = await db.product.findMany(); // runs on the server
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
(db stands for whatever data access you use; Level 2 lesson 08 sets up a real one.)
The query runs on the server, the HTML arrives already containing the list, and this
component adds zero bytes to the browser's JavaScript bundle. The loading and error
states move into sibling files (loading.tsx, error.tsx) covered in Level 2.
When plain React is still the better choice¶
Next.js is not free. It adds a server (unless you use static export), a build step with its own rules, and concepts — server/client boundaries, caching — that a pure client app doesn't have. Plain React with Vite can be the better fit when:
- the app lives entirely behind a login and SEO and first paint don't matter much (internal dashboards, admin tools);
- you already have a separate backend and want the frontend to be static files only;
- you're embedding React widgets in a page that some other system renders.
Next.js pays off for public sites, content that must be indexable, apps where first load matters on slow devices, and full-stack apps where one team owns both UI and data access.
App Router vs Pages Router¶
Next.js has two routers. The Pages Router (pages/ directory) was the original,
built around getServerSideProps and getStaticProps functions that fetched data for
a whole page. The App Router (app/ directory), introduced in version 13, is built
on React Server Components, nested layouts, streaming and Server Actions. Both are
still supported and can coexist in one project during a migration, but new features
land in the App Router and this course uses it exclusively. When you meet
getServerSideProps in an older codebase, think of it as "the Pages Router's way of
doing what an async Server Component does now".
How It Actually Works¶
When a request for /products hits next start, roughly this happens:
- Route matching. Next.js matches the URL against a manifest generated at build
time from your
app/folder. It findsapp/layout.tsx(the root layout) andapp/products/page.tsx. - Server rendering. React renders the tree on the server. Server Components run
to completion (including their
awaits) and produce the RSC payload — a serialised description of the rendered UI in which Client Components appear as references ("render component X from chunk Y with these props") rather than code. - HTML generation. From that payload React also produces HTML, so the browser can paint before any JavaScript runs.
- Hydration. The browser receives the HTML, the RSC payload (inlined in the page) and the JavaScript chunks for only the Client Components on the page. React attaches event handlers to the existing HTML.
- Client navigation. When the user clicks a
<Link>, the browser does not load a new HTML document. The Next.js client router fetches the RSC payload for the next route and React reconciles it into the page, preserving layouts that didn't change.
If the page doesn't depend on anything request-specific, steps 2–3 happen once during
next build and the result is stored as a file. That is "static rendering", and it is
the default whenever Next.js can prove it is safe.
Common mistakes¶
- Treating Next.js as "React with SSR bolted on". Reaching for
useEffect+fetchin every component throws away the main advantage. Fetch in Server Components first; use client fetching only for data that genuinely changes after load. - Marking the root layout
"use client". This turns the whole tree into Client Components and ships all of it to the browser. Push"use client"down to the interactive leaves (lesson 05). - Mixing Pages Router tutorials into App Router code.
getServerSideProps,next/routerand_app.tsxdo nothing inapp/. Check which router a snippet was written for. - Assuming "server rendered" means "rendered on every request". Most App Router pages are rendered at build time. Lesson 09 and Level 2 lesson 04 explain what makes a page dynamic.
Exercise¶
- Take a small React app you have (or the product list above) and list, for each row of the "gaps" table, how that app currently solves it — or doesn't.
- For each of these, decide whether you'd pick Next.js or a plain Vite React app, and write one sentence of justification: a public recipe site; an internal inventory screen used by five people; a marketing site with a signup form; a browser extension popup.
- Predict, before reading lesson 05: in the Next.js
ProductsPageabove, would adding anonClickhandler to the<li>work? Why or why not?