Next.js từ Cơ bản đến Nâng cao: Tổng hợp kiến thức cần biết
Next.js từ Cơ bản đến Nâng cao: Tổng hợp kiến thức cần biết
Bài viết tổng hợp kiến thức về Next.js — framework React phổ biến nhất cho việc xây dựng ứng dụng web full-stack — theo lộ trình từ cơ bản đến nâng cao, kèm ví dụ code minh họa (tập trung vào App Router, chuẩn hiện đại từ Next.js 13+).
Mục lục
- Giới thiệu chung về Next.js
- Cài đặt và cấu trúc project
- App Router và File-based Routing
- Dynamic Routes
- Layout lồng nhau (Nested Layouts)
- Server Component và Client Component
- Data Fetching
- Các chiến lược Render: SSR, SSG, ISR
- Route Handlers (API Routes)
- Server Actions
- Loading UI và Streaming
- Error Handling
- Middleware
- Tối ưu hình ảnh và Font
- Metadata và SEO
- Environment Variables
- Xác thực (Authentication)
- Kết nối Database
- Caching trong Next.js
- Testing trong Next.js
- Triển khai (Deployment)
- So sánh Pages Router và App Router
- Best Practice
- Tài liệu tham khảo
1. Giới thiệu chung về Next.js
Next.js là framework React được phát triển bởi Vercel, ra mắt năm 2016, giúp xây dựng ứng dụng web full-stack (cả frontend lẫn backend) trên nền React. Các điểm nổi bật:
- Nhiều chiến lược render: SSR (Server-side Rendering), SSG (Static Site Generation), ISR (Incremental Static Regeneration), và CSR (Client-side Rendering) — có thể kết hợp linh hoạt trong cùng một app.
- File-based Routing: cấu trúc thư mục quyết định route, không cần cấu hình router thủ công như
react-router-dom. - React Server Components (RSC): component mặc định chạy ở server, giảm lượng JavaScript gửi về client.
- Tối ưu sẵn: tự động tối ưu hình ảnh, font, code splitting, prefetching.
- Full-stack: có thể viết API ngay trong project (Route Handlers) mà không cần backend riêng cho các tác vụ đơn giản.
So với React thuần (CSR), Next.js giải quyết tốt vấn đề SEO và tốc độ tải trang đầu (First Contentful Paint) nhờ render HTML sẵn ở server.
2. Cài đặt và cấu trúc project
npx create-next-app@latest my-app
cd my-app
npm run dev
Cấu trúc project cơ bản (App Router):
my-app/
├── app/
│ ├── layout.tsx # Layout gốc, bắt buộc phải có
│ ├── page.tsx # Trang chủ ("/")
│ ├── globals.css
│ └── gia-su/
│ └── page.tsx # Route "/gia-su"
├── public/ # Static assets
├── components/
├── lib/
├── next.config.js
└── package.json
3. App Router và File-based Routing
Trong App Router, mỗi thư mục trong app/ tương ứng với một segment của URL, và file page.tsx bên trong quyết định nội dung route đó.
// app/page.tsx -> route "/"
export default function TrangChu() {
return <h1>Trang chủ</h1>;
}
// app/gia-su/page.tsx -> route "/gia-su"
export default function TrangGiaSu() {
return <h1>Danh sách gia sư</h1>;
}
// app/gia-su/dang-ky/page.tsx -> route "/gia-su/dang-ky"
export default function DangKyGiaSu() {
return <h1>Đăng ký làm gia sư</h1>;
}
Các file có ý nghĩa đặc biệt trong mỗi thư mục route:
| File | Vai trò |
|---|---|
page.tsx |
Nội dung UI của route, bắt buộc để route hiển thị công khai |
layout.tsx |
Giao diện bao ngoài, dùng chung cho route con |
loading.tsx |
UI hiển thị khi đang tải (Suspense tự động) |
error.tsx |
UI hiển thị khi có lỗi runtime |
not-found.tsx |
UI hiển thị khi gọi notFound() hoặc route không tồn tại |
route.ts |
Định nghĩa API endpoint (Route Handler) |
4. Dynamic Routes
// app/gia-su/[id]/page.tsx -> route "/gia-su/123"
export default function ChiTietGiaSu({ params }: { params: { id: string } }) {
return <p>Chi tiết gia sư ID: {params.id}</p>;
}
Catch-all Routes
// app/blog/[...slug]/page.tsx -> khớp "/blog/a", "/blog/a/b", "/blog/a/b/c"...
export default function Blog({ params }: { params: { slug: string[] } }) {
return <p>Đường dẫn: {params.slug.join("/")}</p>;
}
generateStaticParams (dùng cho SSG với dynamic route)
export async function generateStaticParams() {
const danhSach = await layDanhSachGiaSu();
return danhSach.map((gs) => ({ id: gs.id.toString() }));
}
5. Layout lồng nhau (Nested Layouts)
// app/layout.tsx (Root Layout - bắt buộc)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="vi">
<body>
<header>Header chung toàn site</header>
{children}
<footer>Footer chung toàn site</footer>
</body>
</html>
);
}
// app/gia-su/layout.tsx (Layout riêng cho nhóm route /gia-su/*)
export default function GiaSuLayout({ children }: { children: React.ReactNode }) {
return (
<div className="gia-su-container">
<aside>Sidebar bộ lọc gia sư</aside>
<main>{children}</main>
</div>
);
}
Layout không bị re-render khi chuyển route giữa các trang con cùng layout, giúp giữ nguyên state (ví dụ: vị trí scroll, trạng thái sidebar) và tăng hiệu năng chuyển trang.
Route Groups (nhóm route không ảnh hưởng URL)
app/
├── (marketing)/
│ ├── layout.tsx # Layout riêng cho trang giới thiệu
│ └── gioi-thieu/page.tsx
└── (app)/
├── layout.tsx # Layout riêng cho khu vực ứng dụng chính
└── dashboard/page.tsx
Thư mục đặt trong dấu ngoặc đơn (ten) không xuất hiện trong URL, chỉ dùng để tổ chức code và áp dụng layout khác nhau cho từng nhóm.
6. Server Component và Client Component
Trong App Router, mọi component mặc định là Server Component — chạy hoàn toàn ở server, không gửi JavaScript của nó về client.
// Server Component (mặc định, không cần khai báo gì thêm)
async function DanhSachGiaSu() {
const danhSach = await fetch("https://api.example.com/gia-su").then((r) => r.json());
return <ul>{danhSach.map((gs: any) => <li key={gs.id}>{gs.ten}</li>)}</ul>;
}
Dùng "use client" khi cần: useState, useEffect, sự kiện (onClick...), hoặc các API chỉ có ở trình duyệt (window, localStorage).
"use client";
import { useState } from "react";
export default function BoDemSo() {
const [soDem, setSoDem] = useState(0);
return <button onClick={() => setSoDem(soDem + 1)}>{soDem}</button>;
}
| Server Component | Client Component | |
|---|---|---|
| Chạy ở đâu | Server | Trình duyệt (sau khi hydrate) |
| Dùng Hook (useState, useEffect) | Không | Có |
| Truy cập trực tiếp DB, file system | Có | Không |
| Gửi JS về client | Không | Có |
| Tương tác người dùng (onClick...) | Không | Có |
Lưu ý: nên đẩy "use client" xuống component nhỏ nhất cần tương tác, giữ phần lớn cây component ở dạng Server Component để tối ưu lượng JS gửi về client.
7. Data Fetching
Fetch trực tiếp trong Server Component (cách khuyến nghị)
async function TrangGiaSu() {
const res = await fetch("https://api.example.com/gia-su", {
cache: "force-cache", // mặc định, cache vĩnh viễn cho đến khi revalidate
});
const danhSach = await res.json();
return <ul>{danhSach.map((gs: any) => <li key={gs.id}>{gs.ten}</li>)}</ul>;
}
Fetch song song để tránh waterfall
async function TrangChiTiet({ params }: { params: { id: string } }) {
const [giaSu, danhGia] = await Promise.all([
layGiaSu(params.id),
layDanhGia(params.id),
]);
return (
<div>
<ThongTinGiaSu data={giaSu} />
<DanhSachDanhGia data={danhGia} />
</div>
);
}
Fetch dữ liệu trong Client Component (khi cần tương tác realtime)
Dùng thư viện như TanStack Query hoặc SWR (do chính đội Vercel phát triển) thay vì useEffect + fetch thủ công.
"use client";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((res) => res.json());
function DanhSachGiaSuRealtime() {
const { data, isLoading } = useSWR("/api/gia-su", fetcher, { refreshInterval: 5000 });
if (isLoading) return <p>Đang tải...</p>;
return <ul>{data.map((gs: any) => <li key={gs.id}>{gs.ten}</li>)}</ul>;
}
8. Các chiến lược Render: SSR, SSG, ISR
| Chiến lược | Khi nào HTML được tạo | Phù hợp cho |
|---|---|---|
| SSG (Static Generation) | Lúc build | Trang ít thay đổi: giới thiệu, blog, tài liệu |
| SSR (Server-side Rendering) | Mỗi request | Dữ liệu thay đổi liên tục, cần luôn mới nhất |
| ISR (Incremental Static Regen.) | Lúc build + tự tái tạo theo chu kỳ | Cân bằng giữa tốc độ tĩnh và dữ liệu cập nhật |
| CSR (Client-side Rendering) | Trên trình duyệt | Nội dung cá nhân hóa cao, tương tác nhiều |
// SSG mặc định (không cấu hình gì thêm, fetch cache mặc định "force-cache")
// SSR - luôn lấy dữ liệu mới mỗi request
async function Trang() {
const data = await fetch("https://api.example.com/data", { cache: "no-store" });
...
}
// ISR - tái tạo lại trang sau mỗi 60 giây
async function Trang() {
const data = await fetch("https://api.example.com/data", {
next: { revalidate: 60 },
});
...
}
9. Route Handlers (API Routes)
// app/api/gia-su/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const danhSach = await layDanhSachGiaSu();
return NextResponse.json(danhSach);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const giaSuMoi = await taoGiaSu(body);
return NextResponse.json(giaSuMoi, { status: 201 });
}
// app/api/gia-su/[id]/route.ts
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
const giaSu = await timGiaSuTheoId(params.id);
if (!giaSu) {
return NextResponse.json({ error: "Không tìm thấy" }, { status: 404 });
}
return NextResponse.json(giaSu);
}
Route Handler phù hợp khi cần expose API cho bên thứ ba (mobile app, webhook) — còn nếu chỉ dùng nội bộ trong chính app Next.js, Server Actions (mục 10) thường là lựa chọn gọn hơn.
10. Server Actions
Server Actions cho phép gọi hàm chạy ở server trực tiếp từ Client Component (thường dùng cho form submit), không cần tự viết API endpoint riêng.
// app/gia-su/actions.ts
"use server";
export async function taoGiaSu(formData: FormData) {
const ten = formData.get("ten") as string;
const moTa = formData.get("moTa") as string;
await db.giaSu.create({ data: { ten, moTa } });
revalidatePath("/gia-su"); // làm mới cache của trang danh sách
}
// app/gia-su/FormTaoGiaSu.tsx
import { taoGiaSu } from "./actions";
export default function FormTaoGiaSu() {
return (
<form action={taoGiaSu}>
<input name="ten" placeholder="Tên gia sư" />
<textarea name="moTa" placeholder="Mô tả" />
<button type="submit">Tạo</button>
</form>
);
}
Dùng Server Action với useTransition (loading state phía client)
"use client";
import { useTransition } from "react";
import { taoGiaSu } from "./actions";
export default function FormTaoGiaSu() {
const [dangXuLy, startTransition] = useTransition();
return (
<form action={(fd) => startTransition(() => taoGiaSu(fd))}>
<input name="ten" />
<button disabled={dangXuLy}>{dangXuLy ? "Đang xử lý..." : "Tạo"}</button>
</form>
);
}
11. Loading UI và Streaming
// app/gia-su/loading.tsx
export default function Loading() {
return <p>Đang tải danh sách gia sư...</p>;
}
Next.js tự động bọc page.tsx bằng <Suspense> với fallback là nội dung loading.tsx, cho phép streaming: phần HTML tĩnh (layout, header) hiển thị ngay, phần dữ liệu chậm hơn sẽ "đổ" vào sau khi sẵn sàng.
import { Suspense } from "react";
export default function TrangChiTiet() {
return (
<div>
<ThongTinCoBan />
<Suspense fallback={<p>Đang tải đánh giá...</p>}>
<DanhSachDanhGia />
</Suspense>
</div>
);
}
12. Error Handling
// app/gia-su/error.tsx
"use client"; // error.tsx bắt buộc phải là Client Component
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<p>Đã có lỗi xảy ra: {error.message}</p>
<button onClick={() => reset()}>Thử lại</button>
</div>
);
}
// app/gia-su/[id]/page.tsx
import { notFound } from "next/navigation";
export default async function ChiTietGiaSu({ params }: { params: { id: string } }) {
const giaSu = await timGiaSuTheoId(params.id);
if (!giaSu) notFound(); // render app/gia-su/[id]/not-found.tsx hoặc not-found.tsx gần nhất
return <p>{giaSu.ten}</p>;
}
13. Middleware
Middleware chạy trước khi request đến route, phù hợp cho xác thực, redirect, ghi log, A/B testing.
// middleware.ts (đặt ở gốc project, ngang hàng với app/)
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/dang-nhap", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};
14. Tối ưu hình ảnh và Font
import Image from "next/image";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
export default function TheGiaSu() {
return (
<div className={inter.className}>
<Image
src="/gia-su-avatar.jpg"
alt="Ảnh đại diện gia sư"
width={200}
height={200}
priority // ưu tiên tải sớm cho ảnh above-the-fold
/>
</div>
);
}
next/image tự động: lazy load ảnh ngoài viewport, tạo nhiều kích thước ảnh phù hợp thiết bị (responsive), chuyển sang định dạng hiện đại (WebP/AVIF), và tránh layout shift nhờ khai báo width/height bắt buộc.
15. Metadata và SEO
// app/gia-su/[id]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
const giaSu = await timGiaSuTheoId(params.id);
return {
title: `${giaSu.ten} — Nền tảng gia sư Đà Nẵng`,
description: giaSu.moTaNgan,
openGraph: {
images: [giaSu.anhDaiDien],
},
};
}
// app/layout.tsx - metadata mặc định toàn site
export const metadata = {
title: {
default: "Nền tảng gia sư Đà Nẵng",
template: "%s | Nền tảng gia sư Đà Nẵng",
},
description: "Kết nối gia sư và học viên tại Đà Nẵng",
};
16. Environment Variables
# .env.local
DATABASE_URL=postgresql://user:password@localhost/mydb
NEXT_PUBLIC_API_URL=https://api.example.com
// Chỉ dùng được ở server (Server Component, Route Handler, Server Action)
const dbUrl = process.env.DATABASE_URL;
// Dùng được cả ở client - PHẢI có tiền tố NEXT_PUBLIC_
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
Lưu ý bảo mật: chỉ biến có tiền tố NEXT_PUBLIC_ mới được bundle vào JavaScript phía client — tuyệt đối không đặt secret key, mật khẩu DB dưới tiền tố này.
17. Xác thực (Authentication)
Thư viện phổ biến nhất hiện nay là Auth.js (trước đây là NextAuth.js) và Clerk.
// auth.ts (Auth.js v5)
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Credentials({
async authorize(credentials) {
const user = await xacThucNguoiDung(credentials);
return user ?? null;
},
}),
],
});
// app/api/auth/[...nextauth]/route.ts
export { GET, POST } from "@/auth";
// Sử dụng trong Server Component
import { auth } from "@/auth";
export default async function TrangCaNhan() {
const session = await auth();
if (!session) return <p>Vui lòng đăng nhập</p>;
return <p>Xin chào, {session.user.name}</p>;
}
18. Kết nối Database
Với Prisma ORM (phổ biến nhất trong hệ sinh thái Next.js)
// schema.prisma
model GiaSu {
id Int @id @default(autoincrement())
ten String
email String @unique
createdAt DateTime @default(now())
}
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
// Sử dụng trong Server Component
import { prisma } from "@/lib/prisma";
export default async function DanhSachGiaSu() {
const danhSach = await prisma.giaSu.findMany();
return <ul>{danhSach.map((gs) => <li key={gs.id}>{gs.ten}</li>)}</ul>;
}
Việc khởi tạo PrismaClient như một singleton toàn cục (đoạn code globalForPrisma) giúp tránh mở quá nhiều connection tới database trong môi trường development, do Next.js hot-reload code liên tục.
19. Caching trong Next.js
Next.js có 4 lớp cache chính, cần hiểu rõ để tránh dữ liệu bị "cũ" không mong muốn:
| Lớp cache | Mục đích |
|---|---|
| Request Memoization | Tránh gọi trùng cùng một fetch request trong 1 lần render |
| Data Cache | Cache kết quả fetch() giữa các request/deployment (điều khiển bằng cache, next.revalidate) |
| Full Route Cache | Cache HTML + RSC payload đã render tại build time |
| Router Cache (client) | Cache trên trình duyệt khi điều hướng giữa các route đã ghé thăm |
// Chủ động xóa cache khi dữ liệu thay đổi
import { revalidatePath, revalidateTag } from "next/cache";
revalidatePath("/gia-su"); // làm mới cache của 1 route cụ thể
revalidateTag("danh-sach-gia-su"); // làm mới mọi fetch được gắn cùng tag
fetch("https://api.example.com/gia-su", {
next: { tags: ["danh-sach-gia-su"] },
});
20. Testing trong Next.js
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import TheGiaSu from "@/components/TheGiaSu";
describe("TheGiaSu", () => {
it("hiển thị đúng tên gia sư", () => {
render(<TheGiaSu ten="Nguyễn Văn An" />);
expect(screen.getByText("Nguyễn Văn An")).toBeInTheDocument();
});
});
Với E2E test (kiểm thử toàn luồng người dùng thực tế), nên dùng Playwright — được chính đội Next.js khuyến nghị chính thức trong tài liệu.
21. Triển khai (Deployment)
Vercel (đơn giản nhất, cùng đội phát triển Next.js)
npm install -g vercel
vercel
Chỉ cần kết nối Git repository, Vercel tự động build và deploy mỗi khi push code, tự cấu hình CDN, Serverless Functions cho Route Handlers.
Self-hosted với Docker
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY /app/.next ./.next
COPY /app/node_modules ./node_modules
COPY /app/package.json ./package.json
EXPOSE 3000
CMD ["npm", "start"]
Nên bật output: "standalone" trong next.config.js để giảm đáng kể kích thước image Docker (chỉ copy đúng phần cần thiết thay vì toàn bộ node_modules).
22. So sánh Pages Router và App Router
| Tiêu chí | Pages Router (cũ, pages/) |
App Router (mới, app/, từ v13+) |
|---|---|---|
| Data fetching | getServerSideProps, getStaticProps |
fetch() trực tiếp trong Server Component |
| Layout | _app.tsx, _document.tsx toàn cục |
Layout lồng nhau theo từng thư mục |
| Server Component | Không hỗ trợ | Mặc định |
| Loading/Error UI | Tự viết thủ công | loading.tsx, error.tsx tự động |
| Trạng thái hiện tại | Vẫn được hỗ trợ, dùng cho project cũ | Khuyến nghị cho project mới |
Với project mới, nên bắt đầu bằng App Router vì đây là hướng phát triển chính thức của Next.js hiện nay.
23. Best Practice
- Mặc định giữ component ở dạng Server Component, chỉ thêm
"use client"khi thực sự cần tương tác hoặc Hook. - Fetch dữ liệu song song (
Promise.all) thay vì tuần tự để tránh request waterfall làm chậm trang. - Đặt
"use client"ở component lá (component nhỏ nhất cần state/sự kiện), không đặt ở component cha bao trùm nhiều phần tĩnh. - Dùng
next/imagevànext/fontthay vì thẻ<img>/import font thủ công để tận dụng tối ưu hóa tự động. - Hiểu rõ chiến lược cache (
cache,next.revalidate,next.tags) cho từng loại dữ liệu — tránh trường hợp dữ liệu hiển thị "cũ" ngoài mong muốn do cache mặc định. - Dùng Server Actions cho các thao tác ghi dữ liệu nội bộ (form, mutation) thay vì tự dựng thêm Route Handler không cần thiết.
- Cấu trúc project theo feature/domain, đặt component, hook, action liên quan gần nhau thay vì tách rời theo loại file.
24. Tài liệu tham khảo
- Next.js Official Documentation
- React Server Components RFC
- Vercel Deployment Documentation
- Auth.js Documentation
Bài viết được tổng hợp nhằm mục đích chia sẻ kiến thức, phù hợp làm tài liệu ôn tập hoặc tra cứu nhanh khi xây dựng ứng dụng full-stack với Next.js.
All rights reserved