Next.js Core Concepts — From Pages Router to App Router (v14 → v15 → v16)
A comprehensive bilingual guide covering rendering, routing, caching in Next.js across versions 14, 15, and 16. Written in English with inline Vietnamese translations for language learners.
Giới thiệu
Nếu bạn chỉ mới dùng Next.js với Pages Router, bài này dành cho bạn. Framework đã thay đổi rất nhiều từ phiên bản 14 đến 16, và mô hình tư duy bạn xây dựng xung quanh getStaticProps và getServerSideProps không còn áp dụng được nữa.
Chúng ta sẽ đi qua:
- cách routing dựa trên file hoạt động trong App Router
- các chiến lược render
- cách caching thay đổi từ “cache mọi thứ” sang “không cache gì” sang “cache có chủ đích”
- những gì thay đổi, cải thiện qua các phiên bản
Bài viết sử dụng định dạng song ngữ: tiếng Anh trước, tiếng Việt trong ngoặc nhọn {}. Đọc tự nhiên bằng tiếng Anh, liếc qua tiếng Việt để hiểu ngữ cảnh.
Pages Router và App Router
Thế giới cũ: Pages Router
Trong Pages Router, mọi file trong pages/ trở thành một route:
pages/
├── index.tsx → /
├── about.tsx → /about
├── blog/
│ ├── index.tsx → /blog
│ └── [slug].tsx → /blog/:slug
└── api/
└── hello.ts → /api/hello
Việc lấy dữ liệu được thực hiện qua các hàm đặc biệt:
// pages/blog/[slug].tsx — Pages Router
export async function getStaticProps({ params }) {
const post = await fetchPost(params.slug);
return { props: { post }, revalidate: 60 };
}
export async function getStaticPaths() {
const slugs = await getAllSlugs();
return {
paths: slugs.map((slug) => ({ params: { slug } })),
fallback: 'blocking',
};
}
export default function BlogPost({ post }) {
return <article>{post.content}</article>;
}
Vấn đề của mô hình này:
- Tất cả component mặc định là client component — chúng gửi JavaScript đến trình duyệt
- Lấy dữ liệu chỉ ở cấp page — bạn không thể fetch trong component lồng nhau mà không truyền prop hoặc fetch phía client
- Layout cần giải pháp tạm — không có nested layout native
Thế giới mới: App Router
Được giới thiệu ở Next.js 13, ổn định ở 14, App Router đảo ngược mọi thứ:
app/
├── layout.tsx → Root layout (wraps everything)
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── layout.tsx → Blog layout (persists across blog pages)
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
└── api/
└── hello/
└── route.ts → /api/hello
Những thay đổi mô hình chính:
| Pages Router | App Router |
|---|---|
| Tất cả là Client Component | Mặc định là Server Component |
getStaticProps / getServerSideProps | Chỉ cần hàm async — fetch trực tiếp trong component |
| Không có nested layout | Nested layout native |
_app.tsx + _document.tsx | layout.tsx ở bất kỳ cấp nào |
API routes in pages/api/ | Route Handlers in app/api/.../route.ts |
// app/blog/[slug]/page.tsx — App Router
// This is a Server Component by default {Đây là Server Component mặc định}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params; // async in v15+
const post = await fetchPost(slug);
return <article>{post.content}</article>;
}
Chú ý: không cần getStaticProps, không cần export default function nhận props từ hàm data. Component chính nó LÀ tầng data.
Routing chi tiết
Quy ước file
App Router dùng tên file đặc biệt để định nghĩa cấu trúc UI:
| File | Mục đích |
|---|---|
page.tsx | UI cho route — làm route có thể truy cập |
layout.tsx | UI dùng chung bọc children — tồn tại qua các lần navigate |
loading.tsx | UI loading hiển thị khi content đang stream |
error.tsx | Error boundary cho route segment |
not-found.tsx | UI cho 404 trong segment |
template.tsx | Giống layout nhưng re-mount khi navigate |
default.tsx | UI mặc định cho parallel route |
route.ts | API endpoint |
Layouts — Tính năng đỉnh nhất
Layout tồn tại xuyên suốt các lần chuyển trang. Chúng không re-render khi bạn navigate giữa các trang cùng cấp:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<Sidebar /> {/* Never re-renders on navigation */}
<main className="flex-1">{children}</main>
</div>
);
}
Layout có thể lồng nhau:
app/
├── layout.tsx → Root: html, body, fonts, providers
├── (marketing)/
│ ├── layout.tsx → Marketing: header, footer
│ ├── page.tsx → /
│ └── pricing/page.tsx → /pricing
└── (dashboard)/
├── layout.tsx → Dashboard: sidebar, auth check
├── page.tsx → /dashboard (if you set it up)
└── settings/page.tsx → /settings
Nhóm route
Bọc folder trong ngoặc tròn () để tổ chức mà không ảnh hưởng URL:
app/
├── (auth)/
│ ├── login/page.tsx → /login (not /auth/login)
│ └── register/page.tsx → /register
├── (shop)/
│ ├── layout.tsx → Different layout for shop pages
│ ├── products/page.tsx → /products
│ └── cart/page.tsx → /cart
Điều này cho phép bạn có nhiều root layout — header/footer khác nhau cho các phần khác nhau của app.
Route động
app/blog/[slug]/page.tsx → /blog/hello-world
app/shop/[...slugs]/page.tsx → /shop/a/b/c (catch-all)
app/docs/[[...slugs]]/page.tsx → /docs OR /docs/a/b (optional catch-all)
// app/blog/[slug]/page.tsx
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// In v14: params was synchronous → params.slug
// In v15+: params is async → await params, then destructure
return <h1>{slug}</h1>;
}
Route song song
Render nhiều page trong cùng layout đồng thời:
app/dashboard/
├── layout.tsx
├── page.tsx
├── @analytics/
│ ├── page.tsx → Analytics panel
│ └── default.tsx → Fallback when not matched
├── @team/
│ ├── page.tsx → Team panel
│ └── default.tsx
// app/dashboard/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div>
{children}
<div className="grid grid-cols-2">
{analytics}
{team}
</div>
</div>
);
}
Trường hợp sử dụng: dashboard, modal giữ URL state, render có điều kiện dựa trên auth.
Route chặn
Hiển thị route trong ngữ cảnh khác — như mở ảnh trong modal trong khi URL cập nhật:
app/
├── feed/
│ ├── page.tsx
│ └── (..)photo/[id]/ → Intercepts /photo/[id] from feed
│ └── page.tsx → Shows in modal
└── photo/[id]/
└── page.tsx → Full page (direct navigation or refresh)
Quy ước: (.)
Middleware → Proxy (v16)
Ở v14-15, middleware.ts chạy ở edge:
// middleware.ts (v14-15)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (!request.cookies.get('token')) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: ['/dashboard/:path*'],
};
Ở v16, đổi thành proxy.ts — chạy trên Node.js runtime mặc định:
// proxy.ts (v16)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
if (!request.cookies.get('token')) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: ['/dashboard/:path*'],
};
Tại sao đổi tên? “gợi ý nó có thể làm mọi thứ. Thực tế, nó là proxy mạng — nó chặn request trước khi chúng đến app. Tên mới làm rõ vai trò. Thêm nữa, chạy trên Node.js cho phép truy cập fs, crypto, và các Node API khác.
Các chiến lược Render
Đây là nơi Next.js trở nên phức tạp nhưng mạnh mẽ. Hãy đi qua từng chiến lược:
Server Component và Client Component
Khối xây dựng cơ bản
// Server Component (default) — NO "use client" directive
export default async function ProductList() {
const products = await db.query('SELECT * FROM products');
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name} - ${p.price}</li>
))}
</ul>
);
}
// Client Component — needs interactivity
'use client';
import { useState } from 'react';
export default function AddToCart({ productId }: { productId: string }) {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>
Add to cart ({count})
</button>
);
}
Khi nào dùng cái nào:
| Server Component | Client Component |
|---|---|
| Lấy dữ liệu | Xử lý sự kiện |
| Truy cập backend trực tiếp | useState, useEffect, hooks |
| Giữ bí mật an toàn | Browser APIs (localStorage, window) |
| Giảm bundle phía client | Thư viện tương tác bên thứ 3 |
Tạo trang tĩnh
Trang được render lúc build. HTML được tạo một lần và phục vụ từ CDN.
// app/blog/[slug]/page.tsx — statically generated at build time
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
Khi nào được chọn: Next.js tự động dùng SSG khi page không có hàm động và tất cả data có thể cache.
Tương đương ở Pages Router: getStaticProps + getStaticPaths.
Render phía server
Trang được render mỗi request. Dữ liệu mới mỗi lần, nhưng chậm hơn.
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export default async function Dashboard() {
// Using cookies() makes this page dynamic (SSR)
const token = (await cookies()).get('session');
const data = await fetchDashboardData(token?.value);
return <DashboardUI data={data} />;
}
Bạn cũng có thể ép SSR rõ ràng:
// Force dynamic rendering
export const dynamic = 'force-dynamic';
Tương đương ở Pages Router: getServerSideProps.
Tái tạo tĩnh tăng dần
Tốt nhất của cả hai: tốc độ tĩnh + cập nhật nền:
// app/products/page.tsx
// Revalidate every 60 seconds
export const revalidate = 60;
export default async function Products() {
const products = await fetch('https://api.store.com/products');
return <ProductGrid products={products} />;
}
Cách hoạt động:
- Request đầu → phục vụ bản cache
- Sau 60s, request tiếp vẫn phục vụ cache NHƯNG kích hoạt tái tạo nền
- Khi tái tạo xong, bản mới thay thế cache cũ
Revalidation theo yêu cầu:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { path, tag } = await request.json();
if (path) revalidatePath(path);
if (tag) revalidateTag(tag);
return Response.json({ revalidated: true });
}
Tương đương ở Pages Router: getStaticProps
Render server theo luồng
Thay vì đợi tất cả data trước khi gửi HTML, stream từng phần khi chúng sẵn sàng:
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<StaticHeader />
{/* Streams in when data is ready */}
<Suspense fallback={<Skeleton />}>
<SlowDataComponent />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
</div>
);
}
async function SlowDataComponent() {
// Takes 2 seconds — but doesn't block the whole page
const data = await fetchSlowAPI();
return <DataTable data={data} />;
}
Quy ước loading.tsx là cú pháp rút gọn cho Suspense:
// app/dashboard/loading.tsx — Automatically wraps page.tsx in Suspense
export default function Loading() {
return <DashboardSkeleton />;
}
Render phía client
Cho UI tương tác cao không cần SEO:
'use client';
import { useEffect, useState } from 'react';
export default function LiveChat() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://chat.example.com');
ws.onmessage = (e) => {
setMessages((prev) => [...prev, JSON.parse(e.data)]);
};
return () => ws.close();
}, []);
return <ChatUI messages={messages} />;
}
Render trước một phần
Tương lai của rendering. PPR kết hợp shell tĩnh với streaming động:
┌──────────────────────────────────────────────┐
│ Static Shell (served from CDN instantly) │
│ ┌────────────────────────────────────────┐ │
│ │ Header, Navigation, Layout │ │
│ │ (pre-rendered at build time) │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌─────────────────────┐ │
│ │ 🕐 Dynamic │ │ 🕐 Dynamic │ │
│ │ User Info │ │ Recommendations │ │
│ │ (streamed) │ │ (streamed) │ │
│ └──────────────┘ └─────────────────────┘ │
└──────────────────────────────────────────────┘
// app/store/page.tsx — PPR in action
import { Suspense } from 'react';
import { cookies } from 'next/headers';
// Static part — rendered at build time
function ProductCatalog() {
return <StaticProductGrid />;
}
// Dynamic part — streamed at request time
async function PersonalizedRecommendations() {
const session = (await cookies()).get('session');
const recs = await getRecommendations(session?.value);
return <RecommendationCarousel items={recs} />;
}
export default function StorePage() {
return (
<div>
<ProductCatalog />
<Suspense fallback={<RecSkeleton />}>
<PersonalizedRecommendations />
</Suspense>
</div>
);
}
PPR qua các phiên bản:
- Cờ thử nghiệm
- Ổn định, bật từng route
- Chiến lược render mặc định — PPR thay thế lựa chọn nhị phân SSG/SSR
Sơ đồ quyết định Render
Does your page use dynamic functions?
(cookies, headers, searchParams, uncached fetch)
YES → Does it need the fastest TTFB?
│
├── YES → PPR (static shell + dynamic holes)
│
└── NO → SSR (full dynamic render)
NO → Does data change periodically?
│
├── YES → ISR (static + background revalidation)
│
└── NO → SSG (pure static, rebuild to update)
Caching — Sự thay đổi lớn nhất
Caching là thứ thay đổi nhiều nhất qua các phiên bản. Hiểu sự thay đổi này là rất quan trọng.
v14: Cache mọi thứ mặc định
Ở Next.js 14, triết lý là cache tích cực:
// Next.js 14 — This fetch is CACHED by default!
const data = await fetch('https://api.example.com/posts');
// Equivalent to: fetch(..., { cache: 'force-cache' })
// To opt OUT of caching, you had to be explicit:
const freshData = await fetch('https://api.example.com/posts', {
cache: 'no-store',
});
Bốn tầng cache ở v14:
| Layer | Cache cái gì | Thời gian |
|---|---|---|
| Request Memoization | Các lệnh fetch() trùng trong cùng render | Mỗi request |
| Data Cache | fetch() Response fetch() trên server | Vĩnh viễn (đến khi revalidate) |
| Full Route Cache | Toàn bộ HTML + RSC payload đã render | Persistent (static routes) |
| Router Cache | RSC payload trên client để navigate | Theo session |
Vấn đề: dev bị bối rối. Data cũ mà không biết tại sao. Caching ẩn khiến debug như ác mộng.
v15: Không cache gì mặc định
Next.js 15 đảo ngược mặc định:
// Next.js 15 — This fetch is NOT cached by default!
const data = await fetch('https://api.example.com/posts');
// Equivalent to: fetch(..., { cache: 'no-store' })
// To opt IN to caching:
const cachedData = await fetch('https://api.example.com/posts', {
cache: 'force-cache',
});
// Or with revalidation:
const isrData = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 },
});
Đây là breaking change. App dựa vào cache ẩn bỗng chậm đi vì mọi request đều đến server gốc.
v16: Cache có chủ đích với use cache
Next.js 16 giới thiệu Cache Component — một mô hình tư duy hoàn toàn mới:
// next.config.ts — Enable Cache Components
import type { NextConfig } from 'next';
const config: NextConfig = {
cacheComponents: true,
};
export default config;
Cache cấp file
// app/products/page.tsx
'use cache';
// The ENTIRE page output is cached
export default async function ProductsPage() {
const products = await db.query('SELECT * FROM products');
return <ProductGrid products={products} />;
}
Cache cấp hàm
// lib/data.ts
import { cacheLife, cacheTag } from 'next/cache';
export async function getProducts() {
'use cache';
cacheLife('hours');
cacheTag('products');
return await db.query('SELECT * FROM products');
}
export async function getUser(id: string) {
'use cache';
cacheLife('minutes');
cacheTag(`user-${id}`);
return await db.query('SELECT * FROM users WHERE id = $1', [id]);
}
Cache cấp component
// components/ProductCard.tsx
async function ProductCard({ id }: { id: string }) {
'use cache';
cacheTag(`product-${id}`);
const product = await getProduct(id);
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
Hồ sơ cache
import { cacheLife } from 'next/cache';
// Built-in profiles:
cacheLife('seconds'); // short TTL
cacheLife('minutes'); // medium TTL
cacheLife('hours'); // long TTL
cacheLife('days'); // very long TTL
cacheLife('weeks'); // rarely changes
cacheLife('max'); // cache as long as possible
// Custom profile:
cacheLife({ stale: 300, revalidate: 60, expire: 3600 });
Invalidation ở v16
import { revalidateTag } from 'next/cache';
// v16: revalidateTag now uses SWR pattern
// Serves stale while revalidating in background
await revalidateTag('products');
// New: updateTag() for immediate invalidation
import { updateTag } from 'next/cache';
await updateTag('products'); // Immediately purges
Bảng so sánh Caching
| Hành vi | v14 | v15 | v16 |
|---|---|---|---|
fetch() default | Có cache | Không cache | Not cached (use 'use cache' to opt in) |
| Opt-in mechanism | cache: 'no-store' to disable | cache: 'force-cache' to enable | 'use cache' directive |
| Độ chi tiết | Per-fetch | Per-fetch | File / Function / Component |
| Cache profiles | N/A | N/A | cacheLife() |
| Tag-based invalidation | revalidateTag() | revalidateTag() | revalidateTag() (SWR) + updateTag() |
| Mô hình tư duy | ”Mọi thứ cache trừ khi nói không | ”Không gì cache trừ khi nói có | ”Đánh dấu cái cần cache, kiểm soát bao lâu |
So sánh phiên bản
Bức tranh toàn cảnh
| Tính năng | Next.js 14 | Next.js 15 | Next.js 16 |
|---|---|---|---|
| React version | React 18 | React 19 | React 19.2 |
| Bundler | Webpack (Turbopack opt-in) | Webpack (Turbopack stable for dev) | Turbopack default (dev + prod) |
| Caching default | Tích cực | Chủ động | Rõ ràng |
| Request APIs | Synchronous | Bất đồng bộ (có cảnh báo) | Bắt buộc bất đồng bộ |
| Middleware | middleware.ts (Edge) | middleware.ts (Edge) | proxy.ts (Node.js) |
| PPR | Experimental | Stable (opt-in) | Mặc định |
| Server Actions | Stable | Enhanced with validation | Trưởng thành |
| React Compiler | N/A | Experimental | Stable (opt-in) |
| Node.js minimum | 18.17 | 18.18 | 20.9 |
next lint | Available | Available | Đã xoá |
| AMP support | Không khuyến khích | Deprecated | Đã xoá |
| Build speed | Baseline | 2-3x faster (Turbopack dev) | 5-10x faster (Turbopack prod) |
Breaking Change mỗi phiên bản
Thay đổi gây lỗi từ v14 sang v15
- Caching đảo ngược:
fetch()không còn cache mặc định - API Request bất đồng bộ:
cookies(),headers(),params,searchParamstrở thành Promise - React 19:
// v14 → v15 migration for cookies
// Before (v14) — synchronous
import { cookies } from 'next/headers';
const token = cookies().get('session');
// After (v15) — asynchronous
import { cookies } from 'next/headers';
const token = (await cookies()).get('session');
// v14 → v15 migration for params
// Before (v14)
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
}
// After (v15)
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}
Thay đổi gây lỗi từ v15 sang v16
- Turbopack mặc định: config Webpack tuỳ chỉnh cần cờ
--webpack - Đổi tên: khác tên file VÀ tên hàm
- Đã xoá: dùng ESLint hoặc Biome trực tiếp
- Đã xoá: tất cả AMP API và config bị xoá
- Bỏ Node 18: tối thiểu là 20.9
- Params bất đồng bộ bắt buộc: không còn cảnh báo, chỉ có lỗi
revalidateTag()yêu cầu cache profile- Đã xoá: dùng biến môi trường
// v15 → v16 migration for middleware
// Before: middleware.ts
export function middleware(request: NextRequest) { /* ... */ }
// After: proxy.ts
export function proxy(request: NextRequest) { /* ... */ }
// v15 → v16 migration for caching
// Before (v15): per-fetch caching
const data = await fetch(url, {
cache: 'force-cache',
next: { revalidate: 3600, tags: ['products'] },
});
// After (v16): use cache directive
async function getProducts() {
'use cache';
cacheLife('hours');
cacheTag('products');
const res = await fetch(url);
return res.json();
}
Server Actions
Server Actions cho phép bạn chạy code server từ client component — như gọi API mà không cần xây API:
// app/actions.ts
'use server';
import { revalidateTag } 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.insert({ title, content });
revalidateTag('posts');
}
// app/new-post/page.tsx
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">Publish</button>
</form>
);
}
Phát triển qua các phiên bản:
- Ổn định, xử lý form cơ bản
- Cải thiện với
useActionState, xử lý lỗi tốt hơn - Trưởng thành, tích hợp với
use cachecho optimistic update
Bảng tóm tắt di chuyển
Từ Pages Router sang App Router
| Pages Router | App Router Equivalent |
|---|---|
pages/index.tsx | app/page.tsx |
pages/blog/[slug].tsx | app/blog/[slug]/page.tsx |
pages/_app.tsx | app/layout.tsx |
pages/_document.tsx | app/layout.tsx (html, body) |
pages/api/hello.ts | app/api/hello/route.ts |
getStaticProps | Chỉ cần fetch trong Server Component |
getStaticPaths | generateStaticParams() |
getServerSideProps | Fetch with cookies()/headers() or dynamic = 'force-dynamic' |
useRouter() (next/router) | useRouter() (next/navigation) + usePathname() + useSearchParams() |
Lệnh nâng cấp nhanh
# Upgrade to v15 from v14
npx @next/codemod@latest upgrade 15
# Upgrade to v16 from v15
npx @next/codemod@canary upgrade latest
Kết luận
Hành trình từ Pages Router đến App Router (v16) đại diện cho sự thay đổi căn bản trong cách chúng ta nghĩ về ứng dụng React:
- Routing: từ file phẳng đến layout lồng nhau với route song song
- Rendering: từ “chọn một chiến lược mỗi trang” sang “kết hợp chiến lược trong một trang” (PPR)
- Caching: từ “tin framework” sang “nói rõ bạn muốn gì”
- Data: từ các hàm đặc biệt sang chỉ… component
async
Nếu bạn bắt đầu mới năm 2026: dùng Next.js 16 với App Router. Pages Router vẫn hoạt động nhưng không có tính năng mới.
Nếu bạn đang di chuyển: đi từng bước v14 → v15 → v16. Codemod mỗi phiên bản xử lý hầu hết công việc. App Router và Pages Router có thể cùng tồn tại trong một project.
Tài liệu tham khảo
- Tài liệu chính thức
- Hướng dẫn nâng cấp từng bước
- Hiểu RSC từ tài liệu React
- Chọn chiến lược phù hợp