Next.js Cơ bản
Giới thiệu
Next.js là React framework cho production, cung cấp SSR, SSG, file-based routing, và nhiều tính năng khác out-of-the-box.
npx create-next-app@latest my-app --typescript --tailwind --app
App Router (Next.js 13+)
Cấu trúc thư mục app/ với file-based routing.
app/
├── layout.tsx # Root layout (wrap tất cả pages)
├── page.tsx # Route: /
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
├── (marketing)/ # Route group (không ảnh hưởng URL)
│ ├── about/
│ │ └── page.tsx # Route: /about
│ └── contact/
│ └── page.tsx # Route: /contact
├── blog/
│ ├── page.tsx # Route: /blog
│ └── [slug]/
│ └── page.tsx # Route: /blog/:slug
└── api/
└── users/
└── route.ts # API Route: GET/POST /api/users
// app/layout.tsx - Root Layout
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
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="vi">
<body className={inter.className}>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
Server Components vs Client Components
// Server Component (default) - chạy trên server
// Có thể async, trực tiếp fetch data, không dùng hooks
// app/products/page.tsx
async function ProductsPage() {
// Fetch data trực tiếp - không cần useEffect
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }, // Cache 1 giờ
}).then((r) => r.json());
return (
<div>
<h1>Products</h1>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
export default ProductsPage;
// Client Component - cần 'use client' directive
'use client';
import { useState } from 'react';
function AddToCartButton({ productId }: { productId: number }) {
const [added, setAdded] = useState(false);
async function handleClick() {
await addToCart(productId);
setAdded(true);
}
return (
<button onClick={handleClick}>
{added ? '✓ Added' : 'Add to Cart'}
</button>
);
}
Quy tắc:
- Server Component: Fetch data, Database access, File system, Secret keys
- Client Component: Event listeners, useState/useEffect, Browser APIs
Tối ưu: Push "use client" xuống leaves của component tree
ServerComponent → ServerComponent → ClientComponent (leaf)
Data Fetching Patterns
// 1. Static (SSG) - build time, tốt cho SEO
async function BlogPage() {
const posts = await fetchPosts();
return <PostList posts={posts} />;
}
export const dynamic = 'force-static'; // Luôn static
// 2. Dynamic (SSR) - mỗi request
async function DashboardPage() {
const data = await fetchUserData();
return <Dashboard data={data} />;
}
export const dynamic = 'force-dynamic';
// 3. Incremental Static Regeneration (ISR)
async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetchProduct(params.id);
return <ProductDetail product={product} />;
}
export const revalidate = 60; // Regenerate sau 60 giây
// 4. Parallel data fetching
async function UserDashboard({ userId }: { params: { userId: string } }) {
// Fetch song song - không await tuần tự
const [user, orders, notifications] = await Promise.all([
fetchUser(userId),
fetchOrders(userId),
fetchNotifications(userId),
]);
return (
<div>
<UserProfile user={user} />
<OrderHistory orders={orders} />
<Notifications items={notifications} />
</div>
);
}
Dynamic Routes
// app/blog/[slug]/page.tsx
interface Props {
params: { slug: string };
searchParams: { preview?: string };
}
// Generate static paths (SSG)
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
// Generate metadata dynamically
export async function generateMetadata({ params }: Props) {
const post = await fetchPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.coverImage],
},
};
}
async function BlogPost({ params, searchParams }: Props) {
const post = await fetchPost(params.slug);
if (!post) {
notFound(); // Trigger not-found.tsx
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
export default BlogPost;
API Routes
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get('page') ?? '1');
const users = await db.user.findMany({
skip: (page - 1) * 10,
take: 10,
});
return NextResponse.json({ users, page });
}
export async function POST(request: NextRequest) {
const body = await request.json();
// Validate
const result = createUserSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: result.error.format() },
{ status: 400 }
);
}
const user = await db.user.create({ data: result.data });
return NextResponse.json(user, { status: 201 });
}
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await db.user.findUnique({
where: { id: Number(params.id) },
});
if (!user) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(user);
}
Middleware
// middleware.ts (ở root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
const isAuthPage = request.nextUrl.pathname.startsWith('/auth');
const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard');
// Redirect unauthenticated users
if (isProtectedRoute && !token) {
return NextResponse.redirect(new URL('/auth/login', request.url));
}
// Redirect authenticated users away from auth pages
if (isAuthPage && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// Add custom header
const response = NextResponse.next();
response.headers.set('X-Request-ID', crypto.randomUUID());
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/auth/:path*'],
};
So sánh Rendering Strategies
| Strategy | Khi nào | Ví dụ |
|---|---|---|
| SSG (Static) | Content ít thay đổi | Blog, Landing page |
| ISR | Content thay đổi vừa | Product page, News |
| SSR | Data real-time, user-specific | Dashboard, Checkout |
| CSR | Interactive, no SEO needed | Admin panel widgets |
| Streaming | Nhiều data sources | Complex dashboard |