Next.js App Router: Hướng dẫn toàn diện giúp bạn làm chủ migration từ Pages Router

07 tháng 4, 2026·6 phút đọc

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: 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, useEffect thoả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ống getStaticProps)
  • fetch('/api/data', { cache: 'no-store' }) — bỏ cache, lấy mới mọi lúc (giống getServerSideProps)
  • fetch('/api/data', { next: { revalidate: 3600 } }) — cache và tự động revalidate sau 3600 giây
  • fetch('/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

  1. Đọc cookies và headers trong Server Components: không sử dụng document.cookie mà 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>
}
  1. 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.

  2. Router đổi API: không còn dùng next/router, thay bằng next/navigation.

  3. 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/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 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 được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗