React: Getting Started with Next.js
Last updated: 2026-08-26
Tom built a blog site using
create-react-app. After launching it, he found that the first-screen load took 3 seconds, search engines couldn’t crawl the content, and he had to manually refresh the CDN cached every time he updated a post. He realized that cliente-side rendering alone wasn’t enough—he needed a framework capable of servidor-side rendering, static generation, and automatic optimization. That’s when Next.js came into the picture.
Next.js, developed and maintained by Vercel, is currently the most popular full-stack React framework. It addresses core issues that React, as a pure cliente-side UI library, cannot handle: routing systems, servidor-side rendering, static site generation, API routing, image optimization, font optimization, middleware, and more. Whether you’re building a blog, an e-commerce site, a SaaS application, or an enterprise-level application, Next.js provides best practices right out of the box.
This lesson begins by clarifying the core concepts and architectural design of Next.js. You’ll learn the differences between the four rendering modes, how the App Router works, and the division of labor between servidor-side and cliente-side components. These foundational concepts serve as the basis for further study of data fetching, deployment, and comprehensive hands-on project work. Through Tom’s real-world migration case study, you’ll gain a clear understanding of the complete technical migration path from CRA to Next.js.
1. What You'll Learn
- CSR vs. SSR vs. SSG vs. ISR: Differences and Selection Criteria for the Four Rendering Modes
- Next.js App Router's File-Based Routing Mechanism
- Design and Reuse of Nested Layout Systems
- Division of Labor and Collaboration Between the Server Component and the Client Component
- Migration Paths for the Pages Router and App Router
- How to Configure Dynamic Routes, Catch-All Routes, and API Routes
- Using route state components such as "Loading," "Error," and "Not Found"
- A Practical Guide to Migrating from a CRA Project to Next.js
- Rendering Mode Selection Decision Tree: Select the optimal solution based on page type and data characteristics
- Performance Metrics: Comparison of First-Screen Load Time, SEO Score, and TTFB (Time to First Byte) Across Different Rendering Modes
2. Conceptual Diagrams
While learning Next.js, Tom drew a flowchart to understand the requisição processing path. When a user requisição arrives, Next.js selects a different rendering path based on the page’s configuration (static, dynamic, or incremental), ultimately returning HTML and enabling interaction on the browser side.
This diagram illustrates four key decision points: First, determine the page type (static/dynamic/incremental/cliente-side); then, select the corresponding rendering engine; generate HTML and return it to the browser; and finally, use the hydration process to enable cliente components on the page to become interactive.
flowchart LR
A[User Request] --> B{Next.js Route Matching}
B -->|Static Page| C[SSG<br/>Generated during build]
B -->|Dynamic Pages| D[SSR<br/>Render on Request]
B -->|Incremental Update| E[ISR<br/>Re-verify on Demand]
B -->|Client Interaction| F[CSR<br/>Browser Execution]
C --> G[Back HTML]
D --> G
E --> G
F --> G
G --> H[Hydration<br/>Activate Interaction]
H --> I[Client Component<br/>Run JS]
3. A Real-Life Scenario
Tom’s blog was originally developed using create-react-app, with all pages rendered on the client side. When users opened an article, they first downloaded an empty HTML template, then waited for the JavaScript to finish loading before fetching data from the API to render the content. This was a disaster for a content-driven website—search engine crawlers only saw blank pages, and the time to first content (TFC) was as long as 3 seconds.
After conducting his research, he found that a "hybrid rendering" approach was needed: blog posts would be statically generated (HTML generated during the build process and distributed via a CDN), user profile pages would be server-side rendered (with the latest data generated for each request), and the comments section would be client-side rendered (with interactions handled in the browser). Next.js happens to provide all of this.
He spent a week migrating his blog from CRA to Next.js, doing three specific things: First, he switched the post pages to SSG, using generateStaticParams to generate HTML for all post pages during the build process; after deployment to Vercel, the CDN serves the pre-rendered pages directly, reducing the time to first content (TFC) to 0.3 seconds. Second, he switched the user profile pages to SSR, fetching the latest data from the database with every request; Third, the comments section was implemented using client-side components, loading only the interactive JavaScript on the browser side. After the migration, the site’s SEO score on Google Lighthouse improved from 45 to 98, and the time to first content (TFC) was reduced by 90%.
Tom also ran into quite a few pitfalls during the migration. For example, he initially used the Pages Router in the pages/ directory, but later found that the App Router worked better, so he spent extra time migrating to it. He recommends using App Router directly for new projects to avoid having to migrate a second time. He also discovered that useRouter and usePathname cannot be used directly in Server Components; instead, this routing logic needs to be extracted to Client Components. These lessons have given him a deep understanding of Next.js’s design philosophy—“server-first, client-on-demand.”
(1) Comparison of Rendering Modes
Next.js offers four rendering modes, each designed for different use cases. Understanding the differences between them is key to choosing the right architecture.
CSR (Client-Side Rendering): The browser downloads an empty HTML shell and then executes JavaScript to render the content. Tom’s blog originally used this model. The advantages are smooth interactivity and reduced load on the server; the disadvantages are slow first-screen loading and poor SEO. It is suitable for interactive applications that require user login, such as dashboards and administrative backends.
SSR (Server-Side Rendering): Each time a user makes a request, the server dynamically generates and returns the complete HTML. Tom used this approach to solve his blog’s SEO issues, but since each request must wait for the server to finish rendering before a response is returned, it places a heavy load on the server. It is suitable for pages that require real-time data, such as news sites and e-commerce product pages.
SSG (Static Site Generation): Generates HTML files for all pages during the build phase; after deployment to a CDN, users access the static files directly. This offers the fastest loading speed and the best SEO, but content updates require a full rebuild. Tom’s blog post pages are a good example of this model—once an article is written, its content remains fixed. Suitable for blogs, documentation sites, and marketing pages.
ISR (Incremental Static Generation): An enhanced version of SSG that allows specific pages to be revalidated and updated on demand after the build, without requiring a full site rebuild. For example, if Tom’s product catalog page is set to revalidate every 60 seconds, new products will appear within 60 seconds of being added. This is ideal for e-commerce product pages and sites with frequently updated content.
The selection strategy can be summarized as follows: use SSG for content-first scenarios, SSR for real-time data, CSR for interaction-intensive scenarios, and ISR for scenarios with frequent updates where you don’t want to rebuild the entire site.
Rendering Mode Performance Comparison Table
| Indicator | SSG | ISR | SSR | CSR |
|---|---|---|---|---|
| First-Screen Load Speed | Fastest | Fast | Average | Slow |
| SEO Friendliness | High | High | High | Low |
| Data Timeliness | Fixed at build time | Updated on demand | Latest data with each request | After the browser loads |
| Server Load | None | Low | High | Low |
| Use Cases | Blogs/Documentation | E-commerce/News | Custom Pages | Backend Management |
| Typical TTFB | <50ms | <100ms | 200-500ms | 200-500ms |
▶ Example 1: Implementing the Four Rendering Modes in Next.js
Output:
URL routing: / → Home, /about → About, /contact → Contact. Navigation without page reload.
The code below demonstrates the implementation of the three rendering modes in Tom's blog. Note that generateStaticParams is called during the build process and returns all possible parameter combinations; Next.js uses this to generate static HTML for all pages. dynamic = 'force-dynamic', on the other hand, forces the page to be re-rendered with every request.
// === Static Generation SSG(Default behavior) ===
// app/blog/[slug]/page.tsx
// Automatically scan all articles during the build process slug,Generate the corresponding HTML Page
// After generation CDN Return static files directly,No server-side rendering required
export async function generateStaticParams() {
// Called during build:Get a list of all articles
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
// Back slug Array,Next.js It will do this for each slug Generate a page
return posts.map((post: any) => ({ slug: post.slug }))
}
async function BlogPost({ params }: { params: { slug: string } }) {
// Each page retrieves data independently when it is rendered.
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
export default BlogPost
// === Server-Side Rendering SSR(Dynamic Rendering) ===
// app/dashboard/page.tsx
// Every user request starts from API Get the latest data,Ensure the data is up to date
export const dynamic = 'force-dynamic' // Force the static cache to close,Leaving every time SSR
async function DashboardPage() {
// This will be executed with every request fetch
const stats = await fetch('https://api.example.com/dashboard/stats').then(r => r.json())
return (
<div>
<h1>Real-Time Dashboard</h1>
<p>Current Online Users:{stats.onlineUsers}</p>
<p>Today's Orders:{stats.todayOrders}</p>
<p>Income for This Month:${stats.monthlyRevenue}</p>
</div>
)
}
export default DashboardPage
// === Incremental Static Generation ISR ===
// app/products/[id]/page.tsx
// Generate static pages during the build process,But every 60 The first visit after X seconds will trigger a regenerate.
async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 60 } // ISR:60 Please try again in a few seconds.
}).then(r => r.json())
return (
<div>
<h1>{product.name}</h1>
<p>Price:${product.price}</p>
<p>Inventory:{product.stock}</p>
<p>Description:{product.description}</p>
</div>
)
}
export default ProductPage
In his blog, Tom opted for a hybrid approach combining SSG and ISR: he uses SSG for article pages to ensure the homepage loads quickly, and ISR for product list pages, which updates prices and inventory every 60 seconds. This approach ensures good SEO scores while also maintaining data timeliness.
(2) App Router File Routing
Next.js 13+ introduces the App Router, which automatically generates routes based on the file system. All you need to do is create a folder named app/ and a file named page.tsx within that folder, and Next.js will automatically map the corresponding URL paths. You no longer need to manually maintain route configuration files—the directory structure is the route structure.
Core File Types
page.tsx— Defines the UI page corresponding to a route; only one file of this type is allowed per folderlayout.tsx— Defines the shared layout for this route segment and all its sub-routes; supports nestingloading.tsx— UI displayed while loading route segments, based on React Suspenseerror.tsx— UI displayed when a routing segment error occurs; can be retried using the reset function in error.tsxnot-found.tsx— 404 page, with precision down to each route segmentroute.ts— Defines API endpoints and supports methods such as GET, POST, PUT, and DELETE
Dynamic Routing and Advanced Modes
Use the [param] syntax to create dynamic routes; for example, app/blog/[slug]/page.tsx matches /blog/hello-world. Use the catch-all syntax [...param] to match multi-level paths; for example, app/docs/[...slug]/page.tsx matches /docs/guide/getting-started/installation.
Layout files support deeply nested layouts—where a parent Layout wraps the content of child pages. Common sections such as the navigation bar, sidebar, and footer only need to be written once; Next.js automatically manages the persistent state of the Layout. When switching pages, the Layout is not re-rendered; only the child pages are updated.
▶ Example 2: Complete Blog Routing Structure
Output:
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
Command executed
│ ├── layout.tsx # → Dashboard-Exclusive Layout(With sidebar navigation)
│ └── settings/
│ └── page.tsx # → /dashboard/settings Settings Page
└── api/
└── hello/
└── route.ts # → /api/hello API Endpoint
Output:
Renders: RootLayout component UI
Route Mapping Reference Table
| File Path | Corresponding URL | Description |
|---|---|---|
app/page.tsx |
/ |
Home |
app/about/page.tsx |
/about |
Static Routes |
app/blog/[slug]/page.tsx |
/blog/:slug |
Dynamic Routing |
app/blog/[slug]/loading.tsx |
/blog/:slug |
Loading states on the same route |
app/dashboard/layout.tsx |
/dashboard/* |
Nested Layout |
app/api/hello/route.ts |
/api/hello |
API endpoint |
// app/layout.tsx - Global Layout
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
</nav>
</header>
<main>{children}</main>
<footer>© 2026 Tom 's blog. All rights reserved.</footer>
</body>
</html>
)
}
// app/dashboard/layout.tsx - Dashboard-Exclusive Layout(Nested within the global layout)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: 240, background: '#f5f5f5', padding: 16 }}>
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/settings">Settings</a>
</nav>
</aside>
<section style={{ flex: 1, padding: 16 }}>{children}</section>
</div>
)
}
// app/blog/[slug]/loading.tsx - Article Details Loading...
export default function Loading() {
return (
<div style={{ padding: 24 }}>
<div style={{ height: 32, width: '60%', background: '#eee', borderRadius: 4, marginBottom: 16 }} />
<div style={{ height: 16, width: '30%', background: '#eee', borderRadius: 4, marginBottom: 24 }} />
<div style={{ height: 200, background: '#eee', borderRadius: 4 }} />
</div>
)
}
(3) Server Component and Client Component
One of Next.js’s revolutionary designs is the Server Component. This marks the first time in the React ecosystem that components have been divided into two execution environments: server-side and client-side. By default, all components in the App Router are Server Components—they run on the server, can directly access the database, the file system, and sensitive environment variables, and, most importantly, do not send any JavaScript to the browser.
When you need interactive functionality (such as clicking a button, entering text, using useState or useEffect, or listening for window events), a server-side component alone isn’t enough. In this case, you need to add the 'use client' declaration at the top of the file. Next.js will then mark that component and all its child components as Client Components and bundle them to be executed on the browser side.
Features of the Server Component
- You can use
async/awaitto retrieve data directly, without needing useEffect or SWR - Direct access to databases, file systems, and backend services
- Do not send JavaScript to the browser to reduce the package size
- Secure access to environment variables (not prefixed with NEXT_PUBLIC_)
- You cannot use browser APIs such as useState, useEffect, and onClick
Features of the Client Component
- Full browser interactivity (state, events, DOM API)
- Can be freely combined with Server Components
- Still supports SSR (server-side rendering of the initial HTML)
- The larger the file, the more JavaScript is sent to the browser
Best Practice Models
It adopts a layered architecture where the "Server Component" acts as the container and the "Client Component" acts as the leaf. The outer-layer Server Component is responsible for fetching data and handling layout, while the inner-layer Client Component is responsible only for widgets that require interaction. In this way, most of the logic and data retrieval are handled on the server side, and the browser runs only the minimum necessary JavaScript.
▶ Example 3: Optimal Division of Responsibilities Between Server and Client Components
Output:
Parent layout with <Outlet />. /users → UserList, /users/:id → UserProfile. Nested routes share parent layout.
The code below illustrates the layered model described in Tom’s blog: “Data retrieval on the server, interactions in the browser.” PostsPage is a Server Component—it fetches data on the server, renders it as HTML, and sends it directly to the browser. Users see the complete page without having to wait for JavaScript to load. PostList is a Client Component—it receives data prepared by the server and is responsible only for search, filtering, and deletion interactions on the browser side. Note that 'use client' is only applied to “leaf components” that require interaction, not to the entire page.
// app/posts/page.tsx - Server Component(Default)
// This component runs on the server.,Do not send any JS Go to the browser
import PostList from './PostList'
async function PostsPage() {
// Directly on the server fetch,Data is being compiled HTML It was already ready at that time
// Users see the full page,None loading Status
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return (
<div>
<h1>All Articles</h1>
<PostList posts={posts} />
</div>
)
}
export default PostsPage
// app/posts/PostList.tsx - Annotation 'use client' Become Client Component
// Only this file will be sent JS Go to the browser,The parent component that contains it(Server Component)No
'use client'
import { useState } from 'react'
interface Post {
id: number
title: string
body: string
}
function PostList({ posts: initialPosts }: { posts: Post[] }) {
// useState Only at 'use client' Available in the component
const [posts, setPosts] = useState(initialPosts)
const [search, setSearch] = useState('')
const filtered = posts.filter(p =>
p.title.toLowerCase().includes(search.toLowerCase())
)
function handleDelete(id: number) {
setPosts(prev => prev.filter(p => p.id !== id))
}
return (
<div>
<input
type="text"
placeholder="Search Articles..."
value={search}
onChange={e => setSearch(e.target.value)}
style={{ width: '100%', padding: 8, marginBottom: 16, border: '1px solid #ddd', borderRadius: 4 }}
/>
{filtered.map(post => (
<div key={post.id} style={{
border: '1px solid #ddd', padding: 16, marginBottom: 8, borderRadius: 8,
display: 'flex', justifyContent: 'space-between', alignItems: 'center'
}}>
<div>
<h2 style={{ margin: 0 }}>{post.title}</h2>
<p style={{ margin: '8px 0', color: '#666' }}>{post.body}</p>
</div>
<button
onClick={() => handleDelete(post.id)}
style={{ padding: '4px 12px', background: '#ff4444', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}
>
Delete
</button>
</div>
))}
{filtered.length === 0 && <p>No matching articles were found.</p>}
</div>
)
}
export default PostList
// pages/old-posts.js - Pages Router Pattern(Compatibility with Legacy Projects)
// If you are maintaining Next.js 12 or earlier projects,Pages Router Usage getStaticProps/getServerSideProps
export async function getStaticProps() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return { props: { posts }, revalidate: 60 }
}
export default function OldPostsPage({ posts }: { posts: Post[] }) {
return (
<div>
<h1>Pages Router Pattern</h1>
{posts.map(p => <p key={p.id}>{p.title}</p>)}
</div>
)
}
Server Component vs Client Component restrictions
| Capability | Server Component | Client Component |
|---|---|---|
| Use useState/useEffect | ❌ | ✅ |
| Use onClick/onChange | ❌ | ✅ |
| Direct database access | ✅ | ❌ |
| Access environment variables (without prefix) | ✅ | ❌ |
| Send JS to the browser | ❌ | ✅ |
| Use async/await | ✅ | ❌ |
| Use useRouter/usePathname | ❌ | ✅ |
| Use requestAnimationFrame | ❌ | ✅ |
Flowchart: Server Component or Client Component?
Components require interaction(Click、Input、Scrolling, etc.)?
├── is → Required useState/useEffect?
│ ├── Yes → add 'use client' → Client Component
│ └── No → only accept props for rendering?
│ ├── is → Retain Server Component
│ └── No → add 'use client'
└── No → keep Server Component
4. Tom's Migration Roadmap
We’ve now covered all the core concepts of Next.js. Next, let’s put this knowledge together and see how Tom migrated his CRA blog to Next.js step by step. This process also serves as the standard approach for adopting Next.js in your own projects.
Tom breaks down the complete process of migrating a blog from CRA to Next.js into five steps, each of which corresponds to a key concept covered in this lesson:
- Step 1: Project Initialization — Create a new project using
create-next-app, select "App Router," and migrate the oldsrc/code to theapp/directory. The key point of this step is to understand the directory differences between "App Router" and "Pages Router." - Step 2: Route Refactoring — Replace CRA’s
react-router-domroute configuration with App Router file-based routing. The previous manual route configuration (<Routes><Route path="..." /></Routes>) has been transformed into an intuitive folder structure. Dynamic routing now uses the[param]syntax instead of the:paramsyntax. - Step 3: Select a Rendering Mode by Page Type — Choose the appropriate rendering strategy for different page types: use SSG (with
generateStaticParamsfor pre-generation) for blog posts, SSR (withdynamic = 'force-dynamic') for user profiles, and CSR + Client Component (with'use client') for comment sections. - Step 4: Layout Integration — Extract the navigation bar and footer code, which were previously duplicated on every page, into a global layout, reducing code duplication by 60%. Use nested layouts in the dashboard area to add sidebar navigation, thereby achieving separation of concerns in the layout.
- Step 5: Optimization and Testing — Add
loading.tsxto implement the skeleton screen during loading, anderror.tsxto implement the error fallback UI. Use Chrome Lighthouse to verify SEO and performance metrics, ensuring that the SEO score after migration reaches 90+.
▶ Example 4: App Router loading.tsx and error.tsx
Output:
useEffect manages side effects. React Router handles navigation. Async data fetching/loading states
// app/dashboard/loading.tsx - Automatically Displayed Loading Status
export default function DashboardLoading() {
return (
<div style={{ padding: 24 }}>
<div style={{ height: 32, width: 200, background: '#f0f0f0', borderRadius: 4, marginBottom: 16 }} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
{[1, 2, 3].map(i => (
<div key={i} style={{
height: 120, background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%', borderRadius: 8, animation: 'shimmer 1.5s infinite',
}} />
))}
</div>
</div>
)
}
// app/dashboard/error.tsx - Automatically Displayed Error Status
'use client'
export default function DashboardError({ error, reset }) {
return (
<div style={{ padding: 40, textAlign: 'center' }}>
<h2>Something went wrong!</h2>
<p style={{ color: '#999' }}>{error.message}</p>
<button onClick={reset} style={{ padding: '8px 24px', cursor: 'pointer' }}>Try again</button>
</div>
)
}
// app/dashboard/page.tsx - It will automatically use the one above loading and error
async function DashboardPage() {
const data = await fetch('https://api.example.com/dashboard').then(r => r.json())
return (
<div>
<h1>Dashboard</h1>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
{data.stats.map(stat => (
<div key={stat.label} style={{ padding: 16, border: '1px solid #eee', borderRadius: 8 }}>
<p style={{ color: '#999', margin: 0 }}>{stat.label}</p>
<p style={{ fontSize: 24, fontWeight: 'bold', margin: '4px 0 0' }}>{stat.value}</p>
</div>
))}
</div>
</div>
)
}
export default DashboardPage
Output:
Infinite loop: setState in useEffect without deps → render → effect → setState → render... Fix: add deps or use empty array [].
▶ Example 5: Nested Layouts and Templates
Output:
Heading: "Dashboard". Button: Try again. Async data fetching/loading states
// app/layout.tsx - Root Layout(Must have html and body)
export default function RootLayout({ children }) {
return (
<html lang="zh">
<body style={{ margin: 0, fontFamily: 'sans-serif' }}>
{children}
</body>
</html>
)
}
// app/(dashboard)/layout.tsx - Dashboard Layout(Route groups have no effect URL)
import Sidebar from '@/components/Sidebar'
export default function DashboardLayout({ children }) {
return (
<div style={{ display: 'flex', minHeight: '100vh' }}>
<Sidebar />
<main style={{ flex: 1, padding: 24 }}>
{children}
</main>
</div>
)
}
// app/(auth)/layout.tsx - Authentication Page Layout(Centered Card)
export default function AuthLayout({ children }) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#f5f5f5' }}>
<div style={{ background: 'white', padding: 32, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.1)', width: 400 }}>
{children}
</div>
</div>
)
}
// app/(auth)/login/page.tsx - Login Page
function LoginPage() {
return (
<>
<h2 style={{ textAlign: 'center' }}>Sign In</h2>
<form style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<input type="email" placeholder="Email" style={{ padding: 8, borderRadius: 4 }} />
<input type="password" placeholder="Password" style={{ padding: 8, borderRadius: 4 }} />
<button type="submit" style={{ padding: 10, background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>
Sign In
</button>
</form>
</>
)
}
export default LoginPage
Output:
URL routing: / → Home, /about → About, /contact → Contact. Navigation without page reload.
❓ FAQ
'use client' when a component needs to use useState, useEffect, onClick, or browser APIs. A common pattern is for a Server Component to fetch data and pass it to an internal Client Component to handle interactions. This way, data retrieval is handled on the server, while interaction logic runs in the browser—the best of both worlds. A common mistake among beginners is adding 'use client' to the entire page—it should only be applied to the smallest interactive leaf nodes. If you find a component that requires neither interaction nor state, do not add 'use client'.pages/ directory and maps routes based on filenames. App Router is the new routing system introduced in Next.js 13. It uses the app/ directory and supports new features such as nested layouts, server components, and streaming rendering. For new projects, it is recommended to use App Router directly; existing projects can be migrated gradually. The getStaticProps/getServerSideProps structure in Pages Router has been replaced by async component + fetch in App Router. The two routing systems can coexist, and during migration, you can gradually move pages from Pages Router to App Router.next build && next start to deploy to any Node.js server, or use Docker for containerized deployment. Netlify, AWS Amplify, and Cloudflare Pages also support Next.js deployment. However, if you’re using advanced features such as ISR and middleware, Vercel offers the best compatibility.pages/ to app/, verifying each one as you move it—there’s no need to migrate everything at once. During migration, note the following: Change getStaticProps to fetch directly within the Server Component; move the Layout to layout.tsx, created under the app/ directory; and migrate the logic from pages/_app.tsx to the root layout.tsx. We recommend starting with the simplest pages and tackling more complex ones once you’ve gained some experience.📖 Summary
- Next.js is a full-stack React framework that offers out-of-the-box features such as routing, rendering optimization, API routing, image optimization, and middleware.
- Four rendering modes: SSG is the fastest and best suited for content-heavy sites; SSR provides real-time performance and is best suited for data-heavy sites; ISR is a compromise (on-demand revalidation); and CSR is best suited for interaction-intensive applications.
- Performance Comparison: SSG has the fastest first-page load with no server-side load; SSR provides real-time data but puts a heavy load on the server; ISR balances speed and real-time performance; CSR is suitable for background interaction scenarios.
- The App Router uses file system-based routing; the directories
app/,page.tsx,layout.tsx,loading.tsx,error.tsx, andnot-found.tsxeach correspond to a separate routing capability. - Dynamic routes use the
[param]syntax, catch-all routes use the[...param]syntax; API routes use theroute.tsfile - Layouts support deep nesting and state persistence—switching to a subpage does not destroy the currently loading layout
- Server Components run on the server by default and do not send JavaScript to the browser; components that require interaction should be declared as Client Components using
'use client'. - Layered architecture: The Server Component serves as a data container, while the Client Component acts as an interactive leaf node, minimizing the size of browser-side JavaScript as much as possible
- Pages Router 's
getStaticProps/getServerSidePropsin App Router in replaced by async component + fetch - Decision-making process for choosing a rendering mode: Does the page content change frequently? Do users need real-time data? Does the page require SEO? Consider all these factors when choosing between SSG, ISR, SSR, and CSR.
- Migration process: CRA ->
create-next-appCreate -> Migrate components to theapp/directory -> Select rendering mode by page -> Extract layout -> Add loading/error states - This course serves as the foundation for subsequent courses on data retrieval with Next.js, CI/CD deployment, component library integration, and comprehensive project hands-on exercises.
- Keep Tom’s lesson in mind: Choose the App Router right from the start for new projects to avoid the cost of a second migration.
📝 Exercises
-
Use
npx create-next-app@latest my-blogto create a new project, ensuring that the App Router directory structure is used. Underapp/, create three page routes:/about,/blog, and/blog/[slug]. Each page should contain at least one title and a description. Runnpm run devand visit each route (/about,/blog,/blog/test-article) to verify that the mappings are correct. -
Implement a global layout (
app/layout.tsx) that includes a top navigation bar (with three links: Home, Blog, and About) and a footer. Next, create a nested layout for the/blog/*route to display the article table of contents on the left side of the article detail page. Open React DevTools in your browser to verify that the layout’s nesting hierarchy is correct and that the navigation bar and sidebar retain their state without re-rendering when switching pages. -
Change the blog homepage to a Server Component that retrieves the list of articles directly from the JSONPlaceholder API (
https://jsonplaceholder.typicode.com/posts) and renders them. Then, create a Client Component to implement keyword search and filtering functionality, allowing users to search and filter articles in real time on the browser side. Ensure that the Server Component handles data retrieval, while the Client Component handles only the interactive filtering logic. Once this is complete, add aloading.tsx(to display a placeholder screen) and aerror.tsx(to display error messages and a retry button) to experience the full routing segment configuration. -
Optional: Deploy your blog to Vercel (free), and observe the ISR revalidation process in the Vercel console—after modifying the data returned by the API, check whether the page automatically updates after the
revalidatetime expires.