Back to blog

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

nextjsreactapp-routerserver-componentsfrontend
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ăngReact thuầnNext.js
RoutingTự cài React RouterFile-based routing tích hợp sẵn
Data fetchinguseEffect / TanStack Query ở clientFetch phía server, có cache, colocated
API endpointsCần backend riêngRoute Handlers trong cùng dự án
SEOQuản lý <head> thủ côngDeclarative Metadata API
Hiệu năngCode splitting thủ côngTự động với Server Components
Tối ưu ảnhThủ côngComponent <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-app

CLI 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ùng app/ ở 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.json

File-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/settings

Dynamic 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 — await data trực tiếp, không cần useEffect
  • 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ốngLoại Component
Fetch data từ API hoặc DBServer Component
Hiển thị nội dung tĩnh hoặc server-fetchedServer Component
Thêm tương tác (click, hover, v.v.)Client Component
Dùng useState, useEffectClient Component
Truy cập window, localStorageClient Component
Đăng ký browser eventsClient Component
Giữ secrets khỏi bundleServer 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ượcKhi nội dung thay đổiUse Case
SSGKhông bao giờ / hiếm khiMarketing, tài liệu, blog
ISRĐịnh kỳ (phút/giờ)Tin tức, danh mục sản phẩm
SSRMỗi requestDashboard, trang cá nhân hóa
CSRReal-time / user-triggeredLive 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:

FileMục đích
page.tsxUI của route — làm cho route công khai truy cập được
layout.tsxUI chia sẻ bao bọc các route con
loading.tsxLoading UI hiển thị khi trang đang streaming
error.tsxError boundary cho route và các con của nó
not-found.tsxUI cho lỗi 404
template.tsxGiống layout nhưng re-render khi điều hướng
route.tsAPI 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/auth

Route 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',
      },
    ],
  },
}

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_URL

Quy 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 id

Cá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-secret

Bà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/subscribe lư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 /docs với layout riêng (sidebar navigation)
  • Tạo các trang /docs/getting-started, /docs/api và /docs/examples
  • Thêm loading.tsx và error.tsx cho 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.