Phase 3: Next.js — Từ React đến Full-Stack

Chào mừng bạn đến với Phase 3! Trong Phase 1 bạn đã học mô hình component của React, và trong Phase 2 bạn đã bổ sung routing, quản lý state, forms và data fetching. Giờ là lúc lên tầm: Next.js lấy tất cả những gì bạn biết về React và thêm vào một lớp siêu năng lực full-stack.
Thời gian học: 6–8 ngày, 1–2 giờ mỗi ngày
Yêu cầu: Phase 2: React Ecosystem
Bạn Sẽ Học Được Gì
Sau khi hoàn thành Phase 3, bạn sẽ có thể:
✅ Khởi tạo dự án Next.js 15 với App Router
✅ Sử dụng file-based routing bao gồm dynamic và nested routes
✅ Viết Server Components và biết khi nào dùng Client Components
✅ Chọn giữa SSR, SSG, ISR và CSR cho từng trang
✅ Xây dựng API endpoints với Next.js Route Handlers
✅ Tạo shared layouts, loading states và error boundaries
✅ Thêm SEO metadata theo cách khai báo
✅ Quản lý environment variables an toàn
Tại Sao Chọn Next.js?
React là một thư viện UI — nó xử lý việc render component, nhưng không cho bạn biết cách điều hướng giữa các trang, fetch data ở server hay xây dựng API endpoints. Next.js là một framework xây dựng trên React cung cấp tất cả những điều này và nhiều hơn nữa.
Những tính năng chính Next.js bổ sung:
| Tính năng | React thuần | Next.js |
|---|---|---|
| Routing | Tự cài React Router | File-based routing tích hợp sẵn |
| Data fetching | useEffect / TanStack Query ở client | Fetch phía server, có cache, colocated |
| API endpoints | Cần backend riêng | Route Handlers trong cùng dự án |
| SEO | Quản lý <head> thủ công | Declarative Metadata API |
| Hiệu năng | Code splitting thủ công | Tự động với Server Components |
| Tối ưu ảnh | Thủ công | Component <Image> tích hợp sẵn |
Khởi Tạo Dự Án
Tạo dự án Next.js 15 mới:
npx create-next-app@latest my-appCLI sẽ hỏi:
- TypeScript? → Yes
- ESLint? → Yes
- Tailwind CSS? → Yes (khuyến nghị)
- Thư mục
src/? → Tùy bạn (hướng dẫn này dùngapp/ở root) - App Router? → Yes (đây là cách hiện đại)
- Turbopack cho
next dev? → Yes (dev server nhanh hơn)
Sau khi thiết lập, cấu trúc liên quan là:
my-app/
├── app/ # Tất cả routes ở đây
│ ├── layout.tsx # Root layout (bao bọc tất cả)
│ ├── page.tsx # Trang chủ (/)
│ └── globals.css
├── public/ # Static assets
├── next.config.ts # Cấu hình Next.js
└── package.jsonFile-Based Routing
App Router dùng hệ thống file làm router. Mỗi thư mục trong app/ có chứa page.tsx sẽ trở thành một route.
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
└── dashboard/
├── layout.tsx → shared layout cho dashboard
├── page.tsx → /dashboard
└── settings/
└── page.tsx → /dashboard/settingsDynamic Routes
Dấu ngoặc vuông tạo dynamic segment:
// app/blog/[slug]/page.tsx
interface Props {
params: Promise<{ slug: string }>
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params
return <h1>Bài viết: {slug}</h1>
}Lưu ý: Trong Next.js 15, params và searchParams là Promise — bạn phải await chúng.
Catch-All Routes
app/docs/[...slug]/page.tsx → /docs/a/b/c (params.slug = ['a','b','c'])
app/docs/[[...slug]]/page.tsx → /docs (optional catch-all, cũng khớp /)Route Groups
Dấu ngoặc đơn tạo nhóm không ảnh hưởng đến URL:
app/
├── (marketing)/
│ ├── layout.tsx ← chỉ bao bọc các trang marketing
│ ├── page.tsx → /
│ └── about/
│ └── page.tsx → /about
└── (dashboard)/
├── layout.tsx ← chỉ bao bọc các trang dashboard
└── dashboard/
└── page.tsx → /dashboardĐiều này cho phép bạn áp dụng các layout khác nhau cho từng phần của site mà không thay đổi URL.
Thay Đổi Tư Duy: Server vs Client Components
Đây là khái niệm quan trọng nhất trong Next.js hiện đại. Mặc định, tất cả component trong App Router là Server Components.
Server Components
- Chạy chỉ trên server — không bao giờ ở browser
- Có thể là
async—awaitdata trực tiếp, không cầnuseEffect - Có thể truy cập những thứ chỉ dùng trên server: database, file system, env secrets
- Không ảnh hưởng đến JavaScript bundle — code của chúng không bao giờ gửi về client
- Không thể dùng browser APIs, event handlers hay React hooks
// app/blog/page.tsx — đây là Server Component mặc định
async function BlogPage() {
// Fetch data trực tiếp — không cần useEffect, không cần loading state
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return (
<ul>
{posts.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}Client Components
Thêm 'use client' ở đầu file để biến thành Client Component:
- Chạy ở browser (và tùy chọn pre-render ở server)
- Có thể dùng React hooks (
useState,useEffect,useContext, v.v.) - Có thể dùng browser APIs và event handlers
- Code của chúng được đưa vào JavaScript bundle
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Đếm: {count}
</button>
)
}Quy Tắc Kết Hợp
Server Components có thể render Client Components, nhưng Client Components không thể render Server Components trực tiếp:
// app/page.tsx (Server Component)
import { Counter } from '@/components/Counter' // Client Component — được!
export default function Page() {
return (
<div>
<h1>Tiêu đề render ở server</h1>
<Counter /> {/* Client Component nhúng trong Server Component */}
</div>
)
}Truyền output của Server Component dưới dạng children vào Client Components:
// ✅ Pattern: Server Component bao bọc Client Component với children render ở server
// app/page.tsx
import { Modal } from '@/components/Modal' // 'use client'
export default function Page() {
return (
<Modal>
<ServerRenderedContent /> {/* Server Component dưới dạng children prop */}
</Modal>
)
}Khi Nào Dùng Loại Nào
| Tình huống | Loại Component |
|---|---|
| Fetch data từ API hoặc DB | Server Component |
| Hiển thị nội dung tĩnh hoặc server-fetched | Server Component |
| Thêm tương tác (click, hover, v.v.) | Client Component |
Dùng useState, useEffect | Client Component |
Truy cập window, localStorage | Client Component |
| Đăng ký browser events | Client Component |
| Giữ secrets khỏi bundle | Server Component |
Nguyên tắc: Mặc định dùng Server Components. Chỉ thêm 'use client' khi cần tương tác hoặc browser APIs. Đẩy 'use client' càng sâu trong cây component càng tốt — giữ cho các lá là interactive, không phải toàn bộ cây.
Các Chiến Lược Render
Next.js hỗ trợ bốn chiến lược render. Bạn chọn theo route dựa trên nhu cầu dữ liệu.
Static Generation (SSG)
Mặc định cho Server Components không có dynamic data:
// Trang này được static generate tại build time
export default async function AboutPage() {
const data = await fetch('https://api.example.com/about', {
cache: 'force-cache' // mặc định — cache vô thời hạn
}).then(r => r.json())
return <div>{data.content}</div>
}Dùng SSG cho các trang ít thay đổi: trang marketing, tài liệu, bài blog.
Incremental Static Regeneration (ISR)
Regenerate trang ở background sau một khoảng thời gian:
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // revalidate mỗi giờ
export default async function BlogPost({ params }: Props) {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json())
return <article>{post.content}</article>
}ISR mang lại điều tốt nhất của cả hai: hiệu năng của trang tĩnh với độ tươi của trang động.
On-Demand Revalidation
Kích hoạt revalidation thủ công (ví dụ từ CMS webhook):
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const { path } = await request.json()
revalidatePath(path) // revalidate một path cụ thể
return Response.json({ revalidated: true })
}Server-Side Rendering (SSR)
Render mỗi request — dùng khi cần data luôn tươi:
// Buộc dynamic rendering (SSR)
export const dynamic = 'force-dynamic'
export default async function DashboardPage() {
// Chạy mỗi request
const data = await fetch('https://api.example.com/live-data', {
cache: 'no-store' // không bao giờ cache
}).then(r => r.json())
return <div>{data.currentValue}</div>
}Dùng SSR cho: dashboard của user, data real-time, trang phụ thuộc vào request headers/cookies.
Client-Side Rendering (CSR)
Hành vi React thuần — fetch data ở browser:
'use client'
import { useQuery } from '@tanstack/react-query'
export function LiveFeed() {
const { data } = useQuery({
queryKey: ['feed'],
queryFn: () => fetch('/api/feed').then(r => r.json()),
refetchInterval: 5000 // poll mỗi 5 giây
})
return <div>{data?.items.map(/* ... */)}</div>
}Dùng CSR cho: data live cần polling, phần tương tác dành riêng cho user, data không nên server-render.
Tóm Tắt Chiến Lược Render
| Chiến lược | Khi nội dung thay đổi | Use Case |
|---|---|---|
| SSG | Không bao giờ / hiếm khi | Marketing, tài liệu, blog |
| ISR | Định kỳ (phút/giờ) | Tin tức, danh mục sản phẩm |
| SSR | Mỗi request | Dashboard, trang cá nhân hóa |
| CSR | Real-time / user-triggered | Live feeds, ứng dụng tương tác |
Layouts
Layouts là UI chia sẻ giữa các routes và tồn tại giữa các lần điều hướng — chúng không re-render khi bạn điều hướng đến route con.
Root Layout
app/layout.tsx là bắt buộc và bao bọc toàn bộ ứng dụng:
// app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'My App',
description: 'Built with Next.js',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<header>Navigation ở đây</header>
<main>{children}</main>
<footer>Footer ở đây</footer>
</body>
</html>
)
}Nested Layouts
Layouts lồng nhau tự động dựa theo cấu trúc thư mục:
app/
├── layout.tsx ← bao bọc tất cả (header + footer)
├── page.tsx
└── dashboard/
├── layout.tsx ← chỉ bao bọc dashboard routes (sidebar)
├── page.tsx
└── settings/
└── page.tsx ← được áp dụng cả hai layouts// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<aside>Sidebar nav</aside>
<main className="flex-1">{children}</main>
</div>
)
}Các File Đặc Biệt
Next.js có nhiều tên file được đặt trước trong App Router:
| File | Mục đích |
|---|---|
page.tsx | UI của route — làm cho route công khai truy cập được |
layout.tsx | UI chia sẻ bao bọc các route con |
loading.tsx | Loading UI hiển thị khi trang đang streaming |
error.tsx | Error boundary cho route và các con của nó |
not-found.tsx | UI cho lỗi 404 |
template.tsx | Giống layout nhưng re-render khi điều hướng |
route.ts | API endpoint (Route Handler) |
Loading UI (Streaming)
Tạo loading.tsx bên cạnh page.tsx của bạn:
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-16 bg-gray-200 rounded" />
))}
</div>
)
}Điều này dùng React Suspense ở bên dưới — loading UI hiển thị ngay lập tức trong khi trang fetch data, rồi thay thế bằng nội dung thực.
Error Boundaries
// app/blog/error.tsx
'use client' // error boundaries phải là Client Components
interface Props {
error: Error & { digest?: string }
reset: () => void
}
export default function Error({ error, reset }: Props) {
return (
<div>
<h2>Có gì đó không ổn!</h2>
<p>{error.message}</p>
<button onClick={reset}>Thử lại</button>
</div>
)
}Route Handlers (API Routes)
Tạo API endpoints trong ứng dụng Next.js của bạn bằng file route.ts:
app/
└── api/
├── posts/
│ ├── route.ts → GET/POST /api/posts
│ └── [id]/
│ └── route.ts → GET/PUT/DELETE /api/posts/:id
└── auth/
└── route.ts → POST /api/authRoute Handler Cơ Bản
// app/api/posts/route.ts
import { NextRequest } from 'next/server'
// GET /api/posts
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const limit = Number(searchParams.get('limit') ?? '10')
const posts = await db.post.findMany({ take: limit })
return Response.json(posts)
}
// POST /api/posts
export async function POST(request: NextRequest) {
const body = await request.json()
if (!body.title) {
return Response.json({ error: 'Tiêu đề là bắt buộc' }, { status: 400 })
}
const post = await db.post.create({ data: body })
return Response.json(post, { status: 201 })
}Dynamic Route Handler
// app/api/posts/[id]/route.ts
interface Context {
params: Promise<{ id: string }>
}
export async function GET(request: NextRequest, { params }: Context) {
const { id } = await params
const post = await db.post.findUnique({ where: { id } })
if (!post) {
return Response.json({ error: 'Không tìm thấy' }, { status: 404 })
}
return Response.json(post)
}
export async function DELETE(request: NextRequest, { params }: Context) {
const { id } = await params
await db.post.delete({ where: { id } })
return new Response(null, { status: 204 })
}Middleware
Middleware chạy trước khi request hoàn thành — hữu ích cho auth, redirects và A/B testing:
// middleware.ts (ở root dự án)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*'],
}Metadata & SEO
Next.js có Metadata API khai báo — không cần quản lý <head> thủ công nữa.
Static Metadata
// app/about/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Về Chúng Tôi',
description: 'Tìm hiểu thêm về đội ngũ của chúng tôi.',
openGraph: {
title: 'Về Chúng Tôi',
description: 'Tìm hiểu thêm về đội ngũ của chúng tôi.',
images: ['/images/about-og.png'],
},
twitter: {
card: 'summary_large_image',
},
}
export default function AboutPage() {
return <h1>Về Chúng Tôi</h1>
}Dynamic Metadata
Generate metadata dựa trên route params:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
interface Props {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
images: [post.ogImage],
},
}
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.content}</article>
}Title Templates
Đặt title template trong root layout để tất cả các trang tự động format tiêu đề:
// app/layout.tsx
export const metadata: Metadata = {
title: {
template: '%s | My Blog',
default: 'My Blog',
},
}
// app/about/page.tsx
export const metadata: Metadata = {
title: 'Về Chúng Tôi', // Render thành "Về Chúng Tôi | My Blog"
}Component next/image
Next.js tối ưu ảnh tự động — phục vụ WebP/AVIF, lazy loading và ngăn layout shift:
import Image from 'next/image'
export default function Avatar() {
return (
<Image
src="/images/avatar.jpg"
alt="Avatar của tôi"
width={400}
height={400}
priority // preload ảnh above-the-fold
/>
)
}Cho ảnh remote, whitelist domain trong next.config.ts:
// next.config.ts
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
],
},
}Component next/link
Dùng <Link> cho client-side navigation — tự động prefetch các trang được liên kết:
import Link from 'next/link'
export function Nav() {
return (
<nav>
<Link href="/">Trang chủ</Link>
<Link href="/blog">Blog</Link>
<Link href="/about">Về tôi</Link>
</nav>
)
}Cho programmatic navigation:
'use client'
import { useRouter } from 'next/navigation'
export function LoginButton() {
const router = useRouter()
async function handleLogin() {
await login()
router.push('/dashboard') // điều hướng sau hành động
router.refresh() // re-fetch server data
}
return <button onClick={handleLogin}>Đăng nhập</button>
}Environment Variables
Next.js có hệ thống environment variable tích hợp sẵn:
.env.local # chỉ dùng khi dev local — đừng commit file này
.env.development # load khi `next dev`
.env.production # load khi `next build`
.env # luôn load (defaults)Biến Chỉ Dùng Ở Server
Biến không có prefix chỉ khả dụng ở server:
# .env.local
DATABASE_URL=postgres://localhost/mydb
JWT_SECRET=super-secret-key
RESEND_API_KEY=re_xxxxxxxx// app/api/route.ts — chỉ server, ổn
const db = createClient(process.env.DATABASE_URL!)Biến Public
Thêm prefix NEXT_PUBLIC_ để expose ra browser:
# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_GA_ID=G-XXXXXXXX// Bất kỳ component nào — khả dụng ở mọi nơi
const apiUrl = process.env.NEXT_PUBLIC_API_URLQuy tắc bảo mật: Đừng bao giờ đặt secrets vào biến NEXT_PUBLIC_ — chúng sẽ gửi đến mọi browser.
Static Params cho Dynamic Routes
Khi dùng SSG với dynamic routes, cho Next.js biết những slug nào cần pre-render:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.content}</article>
}Tại build time, Next.js gọi generateStaticParams, build một trang cho mỗi slug và phục vụ chúng dưới dạng file tĩnh — không cần server lúc runtime.
Caching Trong Next.js
Next.js có hệ thống cache tích cực. Hiểu nó là chìa khóa để có hành vi đúng.
Các tùy chọn fetch quan trọng:
// Cache vô thời hạn (mặc định cho Server Components không có revalidate)
fetch(url, { cache: 'force-cache' })
// Không bao giờ cache — luôn fetch tươi
fetch(url, { cache: 'no-store' })
// Revalidate mỗi N giây
fetch(url, { next: { revalidate: 3600 } })
// Tag để on-demand revalidation
fetch(url, { next: { tags: ['posts'] } })Revalidate với tags:
// Trong Server Action hoặc Route Handler:
import { revalidateTag } from 'next/cache'
revalidateTag('posts') // hủy cache tất cả fetch được tag 'posts'Server Actions
Server Actions cho phép gọi code phía server trực tiếp từ Client Components — không cần Route Handler cho các mutation đơn giản:
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.post.create({ data: { title, content } })
revalidatePath('/blog')
}Dùng trong form:
// app/new-post/page.tsx
import { createPost } from '@/app/actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Tiêu đề" />
<textarea name="content" placeholder="Nội dung" />
<button type="submit">Tạo bài viết</button>
</form>
)
}Hoặc gọi từ Client Component:
'use client'
import { createPost } from '@/app/actions'
import { useTransition } from 'react'
export function PostForm() {
const [isPending, startTransition] = useTransition()
function handleSubmit(formData: FormData) {
startTransition(async () => {
await createPost(formData)
})
}
return (
<form action={handleSubmit}>
<input name="title" />
<button type="submit" disabled={isPending}>
{isPending ? 'Đang tạo...' : 'Tạo bài viết'}
</button>
</form>
)
}Kết Hợp Tất Cả: Ứng Dụng Blog
Đây là kiến trúc của ứng dụng blog hoàn chỉnh sử dụng tất cả những gì đã đề cập:
Cấu trúc file:
app/
├── layout.tsx # Root layout: fonts, providers
├── page.tsx # Trang chủ: bài viết gần đây (ISR)
├── blog/
│ ├── layout.tsx # Blog layout: sidebar, breadcrumb
│ ├── page.tsx # Tất cả bài viết (ISR 1 giờ)
│ ├── loading.tsx # Skeleton khi đang tải
│ └── [slug]/
│ ├── page.tsx # Nội dung bài (SSG)
│ └── not-found.tsx # Bài viết không tìm thấy
├── dashboard/
│ ├── layout.tsx # Dashboard layout: sidebar nav
│ └── page.tsx # Thống kê user (SSR, auth-protected)
└── api/
└── posts/
├── route.ts # GET list, POST tạo mới
└── [id]/
└── route.ts # GET, PUT, DELETE theo idCác Lỗi Thường Gặp Cần Tránh
1. Thêm 'use client' quá cao
// ❌ Sai: toàn bộ trang là Client Component
'use client'
export default function BlogPage() {
const posts = // không thể fetch phía server nữa
return <div>...</div>
}
// ✅ Đúng: chỉ phần tương tác là Client Component
export default async function BlogPage() { // Server Component
const posts = await getPosts()
return (
<div>
{posts.map(p => <PostCard key={p.id} post={p} />)}
<SearchBar /> {/* Client Component cho tương tác */}
</div>
)
}2. Dùng useEffect để fetch data trong Server Components
// ❌ Sai: fetch phía client không cần thiết
'use client'
export default function BlogPage() {
const [posts, setPosts] = useState([])
useEffect(() => {
fetch('/api/posts').then(r => r.json()).then(setPosts)
}, [])
return <div>{posts.map(/*...*/)}</div>
}
// ✅ Đúng: fetch trực tiếp trong Server Component
export default async function BlogPage() {
const posts = await getPosts() // chạy ở server, không ảnh hưởng bundle
return <div>{posts.map(/*...*/)}</div>
}3. Quên await params
// ❌ Sai trong Next.js 15
export default function Page({ params }) {
const { slug } = params // params là Promise trong Next.js 15
}
// ✅ Đúng
export default async function Page({ params }) {
const { slug } = await params
}4. Đặt secrets vào biến NEXT_PUBLIC_
# ❌ Sai: gửi về browser
NEXT_PUBLIC_DATABASE_URL=postgres://...
NEXT_PUBLIC_JWT_SECRET=my-secret
# ✅ Đúng: chỉ server
DATABASE_URL=postgres://...
JWT_SECRET=my-secretBài Tập Thực Hành
Bài tập 1: Tạo blog Next.js với:
- Trang chủ hiển thị 3 bài viết gần đây (SSG với ISR)
- Trang danh sách blog với tất cả bài viết
- Các trang bài viết riêng lẻ với
generateStaticParams - Dynamic metadata cho mỗi bài viết
Bài tập 2: Thêm newsletter API:
POST /api/subscribelưu email vào file JSON- Form đăng ký (Client Component)
- Phiên bản Server Action của cùng form đó
Bài tập 3: Xây dựng dashboard được bảo vệ:
- Middleware redirect người dùng chưa xác thực đến
/login - Form đăng nhập đơn giản set cookie
- Trang dashboard đọc cookie và hiển thị thông tin user
- Nút đăng xuất xóa cookie
Bài tập 4: Thử thách nested layouts:
- Tạo section
/docsvới layout riêng (sidebar navigation) - Tạo các trang
/docs/getting-started,/docs/apivà/docs/examples - Thêm
loading.tsxvàerror.tsxcho docs route
Tóm Tắt
Next.js biến React từ thư viện UI thành framework full-stack hoàn chỉnh. Đây là những gì bạn đã học:
✅ File-based routing: các thư mục trong app/ trở thành URL paths
✅ Server vs Client Components: server mặc định, 'use client' cho tương tác
✅ Chiến lược render: SSG, ISR, SSR và CSR — chọn theo từng trang
✅ Layouts: UI chia sẻ tồn tại giữa các điều hướng
✅ Các file đặc biệt: loading.tsx, error.tsx, not-found.tsx
✅ Route Handlers: API endpoints colocated với UI
✅ Metadata API: SEO khai báo không cần quản lý <head> thủ công
✅ Environment variables: chỉ server vs NEXT_PUBLIC_
✅ Caching: force-cache, no-store, revalidate và tags
✅ Server Actions: gọi code server từ Client Components mà không cần Route Handlers
Sự thay đổi tư duy — Server Components mặc định, Client Components chỉ khi cần — là chìa khóa để viết ứng dụng Next.js hiệu năng và bảo mật.
Series: React.js & Next.js Learning Roadmap
Trước: Phase 2: React Ecosystem
Tiếp theo: Deep Dive: React Hooks Mastery (Sắp ra mắt)
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.