Next.js App Router: Hướng dẫn toàn diện giúp bạn làm chủ migration từ Pages Router
Bài viết tổng hợp những trải nghiệm thực tế và lời khuyên hữu ích khi chuyển từ Pages Router sang App Router trong Next.js, tập trung vào thay đổi suy nghĩ về Server và Client Components, cơ chế fetch dữ liệu cùng caching, structure folder và các lỗi thường gặp trong quá trình triển khai.

Next.js App Router: Hướng dẫn toàn diện giúp bạn làm chủ migration từ Pages Router
Next.js App Router mang đến một chuyển đổi nền tảng trong cách xây dựng ứng dụng React hiện đại. Bài viết này tổng hợp kinh nghiệm cá nhân của tác giả khi chuyển từ Pages Router sang App Router, nhằm giúp bạn hiểu rõ những điểm mấu chốt cần lưu ý để tránh mất thời gian debug và tận dụng tối đa sức mạnh của App Router.
Thay đổi tư duy quan trọng khi dùng App Router
Điểm sai lầm lớn nhất khi mới bắt đầu là coi App Router chỉ đơn giản là Pages Router với cấu trúc thư mục khác, trong khi thực tế đây là một paradigm hoàn toàn khác.
- Trong Pages Router, mặc định các component là Client Components. Bạn có thể sử dụng các hook React như
useState,useEffectthoải mái, và dùng các phương thức fetch dữ liệu truyền thống nhưgetServerSideProps. - Trong App Router, mọi component mặc định là Server Components — nghĩa là code chạy trên server, không được bundle sang client, không thể sử dụng hook React dành cho client và cũng không truy cập được DOM hay đối tượng toàn cục như
window.
Ví dụ lỗi thường gặp:
// app/dashboard/page.tsx
export default function Dashboard() {
const [count, setCount] = useState(0) // 💥 lỗi runtime: useState không phải là function
return <div>{count}</div>
}
Cách khắc phục là đánh dấu rõ ràng component cần tương tác trên client bằng câu lệnh "use client":
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(c => c + 1)}>
Clicks: {count}
</button>
)
}
Nguyên tắc vàng: Server Components theo mặc định, Client Components chỉ khi cần tương tác UI, trạng thái trình duyệt hoặc xử lý sự kiện DOM.
Cấu trúc thư mục chuẩn của App Router
App Router sử dụng thư mục /app với các quy tắc cụ thể:
app/
├── layout.tsx ← Layout gốc (bắt buộc)
├── page.tsx ← Route tại / (trang chủ)
├── loading.tsx ← UI hiển thị khi tải (streaming)
├── error.tsx ← Xử lý lỗi
├── not-found.tsx ← Trang 404
├── dashboard/
│ ├── layout.tsx ← Layout con cho /dashboard
│ ├── page.tsx ← Route /dashboard
│ └── settings/
│ └── page.tsx ← Route /dashboard/settings
└── api/
└── webhook/
└── route.ts ← API Routes
Điểm nổi bật là layout lồng nhau (nested layouts), giúp giữ trạng thái và UI bao quanh không bị tái tạo khi chuyển giữa các route con, khác với Pages Router vốn không làm tốt điều này.
Ví dụ layout con trong /dashboard:
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<Sidebar /> {/* render một lần, không bị unmount giữa chuyển trang */}
<main className="flex-1">{children}</main>
</div>
)
}
Fetch dữ liệu: quên đi các phương pháp cũ
Không còn getServerSideProps hay getStaticProps nữa, App Router cho phép bạn fetch dữ liệu trực tiếp trong Server Component bằng async/await:
// app/products/page.tsx
async function getProducts() {
const res = await fetch('https://api.tudominio.com/products', { next: { revalidate: 60 } })
if (!res.ok) throw new Error('Không thể tải products')
return res.json()
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
Cơ chế cache mặc định của Next.js
Next.js cache kết quả fetch theo mặc định để tăng hiệu suất và giảm tải server:
fetch('/api/data')— cache không giới hạn (giốnggetStaticProps)fetch('/api/data', { cache: 'no-store' })— bỏ cache, lấy mới mọi lúc (giốnggetServerSideProps)fetch('/api/data', { next: { revalidate: 3600 } })— cache và tự động revalidate sau 3600 giâyfetch('/api/data', { next: { tags: ['products'] } })— cache kèm tag để có thể invalidate thủ công
Ví dụ invalidate cache với tag:
// app/api/update-product/route.ts
import { revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const body = await request.json()
await updateProductInDB(body)
revalidateTag('products') // Invalidate cache tag 'products'
return Response.json({ ok: true })
}
Streaming và Suspense: trải nghiệm người dùng cao cấp
App Router hỗ trợ streaming server rendering kết hợp với React Suspense, cho phép gửi từng phần UI về trình duyệt ngay khi chúng sẵn sàng mà không trì hoãn toàn bộ trang.
Ví dụ Dashboard với dữ liệu nhanh và chậm:
import { Suspense } from 'react'
import { UserStats } from './UserStats' // nhanh
import { SalesChart } from './SalesChart' // chậm
import { RecentOrders } from './RecentOrders'
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<StatsSkeleton />}>
<UserStats />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<SalesChart />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
</div>
)
}
Mỗi Suspense boundary tự resolve riêng biệt, giúp UI hiển thị nhanh hơn nhiều so với chờ tất cả dữ liệu hoàn thành.
Những lỗi hay gặp và cách khắc phục
- Đọc cookies và headers trong Server Components: không sử dụng
document.cookiemà phải dùng API của Next.js:
import { cookies, headers } from 'next/headers'
export default async function Page() {
const cookieStore = await cookies()
const token = cookieStore.get('auth-token')
const headersList = await headers()
const userAgent = headersList.get('user-agent')
return <div>Token: {token?.value}</div>
}
-
Không thể truyền hàm callback từ Server Component sang Client Component do hàm JS không serialize được. Hàm phải định nghĩa trong Client Component.
-
Router đổi API: không còn dùng
next/router, thay bằngnext/navigation. -
Biến môi trường: Server Component có thể dùng bất kỳ biến môi trường nào, Client Component chỉ được sử dụng biến tiền tố
NEXT_PUBLIC_.
Chiến lược migration từng bước
- Bắt đầu bằng chuyển layouts gốc sang
layout.tsx - Sau đó xử lý các trang tĩnh đơn giản
- Tiếp theo mới đến các trang có fetch dữ liệu server
- Cuối cùng xử lý phần authentication
Next.js cho phép 2 thư mục /pages và /app hoạt động song song, bạn có thể deploy từng phần, giảm thiểu rủi ro.
Có nên chuyển hoàn toàn sang App Router?
Câu trả lời là có, cực kỳ xứng đáng.
Tác giả chứng kiến dự án e-commerce giảm Time To First Byte từ 340ms xuống 89ms, Largest Contentful Paint từ 3.2s xuống 1.1s, và bundle JavaScript giảm 60%, đồng thời trải nghiệm người dùng cải thiện rõ rệt.
Sau khi quen với mô hình Server Components mặc định và Client Components có chọn lọc, việc phát triển ứng dụng React trở nên sạch sẽ, rõ ràng và hiệu quả hơn rất nhiều.
"Giờ đây tôi không thể tưởng tượng quay lại Pages Router, nó giống như dùng jQuery sau khi đã biết React. Về kỹ thuật thì vẫn chạy, nhưng bạn biết rằng có công cụ tốt hơn."
Nguồn: Bài viết được chuyển ngữ và tổng hợp từ juanchi.dev
Bài viết liên quan

Phần mềm
Anthropic ra mắt Claude Opus 4.7: Nâng cấp mạnh mẽ cho lập trình nhưng vẫn thua Mythos Preview
16 tháng 4, 2026

Công nghệ
Qwen3.6-35B-A3B: Quyền năng Lập trình Agentic, Nay Đã Mở Cửa Cho Tất Cả
16 tháng 4, 2026

Công nghệ
Spotify thắng kiện 322 triệu USD từ nhóm pirate Anna's Archive nhưng đối mặt với bài toán thu hồi
16 tháng 4, 2026
