Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

StrategyKhi nàoVí dụ
SSG (Static)Content ít thay đổiBlog, Landing page
ISRContent thay đổi vừaProduct page, News
SSRData real-time, user-specificDashboard, Checkout
CSRInteractive, no SEO neededAdmin panel widgets
StreamingNhiều data sourcesComplex dashboard