“React hay Next.js?” nghe giống một lựa chọn ngang hàng, nhưng thực ra một bên là thư viện UI còn bên kia là framework dùng React để tổ chức cả website. Mình viết bài này sau khi phải chọn stack cho chính BloodyCoTech: blog cần URL và HTML tĩnh; game nhỏ lại có thể chỉ cần một client app.

Phần API bên dưới bám React 19 và Next.js 16. Mình ghi phiên bản vì các ví dụ cũ về App Router, đặc biệt là params, rất dễ không còn đúng.

React làm phần giao diện

React là thư viện UI. Bạn viết component, giữ state, và mô tả màn hình theo dữ liệu. React không bắt buộc một router, không tạo file HTML cho từng URL, và không có ý kiến về chỗ đặt máy chủ.

Một component có state phải chạy trên client:

tsx
"use client";
 
import { useState } from "react";
 
export function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <button type="button" onClick={() => setCount((value) => value + 1)}>
      {count}
    </button>
  );
}

useState chỉ có nghĩa trong trình duyệt hoặc trong một client component. Nếu bạn chỉ cần một màn hình tương tác, React cùng một bundler như Vite là đủ. Không có thêm tầng framework nào bắt buộc.

Next.js thêm khung của một website

Next.js là framework bọc React. App Router dùng thư mục app/ để thành URL. page.tsx là một route, layout.tsx là khung dùng lại. Component mặc định là Server Component: nó render thành HTML lúc build hoặc trên server, và không được gọi useState.

Từ Next.js 15, params của một trang động là Promise. Cách đọc slug vì vậy là await, không phải lấy object đồng bộ như các ví dụ cũ:

tsx
export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <article>{slug}</article>;
}

Khi một phần cây cần sự kiện hoặc state, file đó mở đầu bằng chỉ thị client. Ranh giới này là thứ React thuần không có, vì React thuần không chia server và client trong cùng một cây file.

So sánh theo việc cần làm

ViệcReact với bundlerNext.js
Một màn hình, một URLPhù hợpThừa nếu không cần các route khác
Nhiều URL, cần HTML riêng cho từng trangTự gắn router và công cụ metaCó sẵn quy ước file
Form gửi dữ liệu lên server của chính appTự làm APIServer Action cần runtime Node
Xuất HTML tĩnh, không giữ serverLà mô hình mặc định của SPAĐược, nếu bật static export và bỏ tính năng server

Bảng này không nói bên nào nhanh hơn. Nó chỉ nói phần việc nào đã có người làm sẵn.

Khi không nên chọn Next.js

Đừng thêm Next.js chỉ vì nó phổ biến. Một game web thường là một canvas hoặc một cây DOM tương tác, ít URL, và không cần HTML riêng cho từng màn chơi. Router, server component và bước export chỉ thêm việc build.

Công cụ nội bộ một trang, widget nhúng vào trang khác, và prototype cần sửa là thấy ngay cũng thường gọn hơn với React và Vite. Bạn vẫn dùng TypeScript, vẫn chia component, và không phải giữ một file cấu hình framework.

Next.js còn dễ bị dùng sai khi người viết copy một ví dụ có server nhưng lại triển khai lên host chỉ nhận file tĩnh. Build sẽ lỗi, hoặc trang chạy ở máy dev rồi vỡ lúc xuất bản. Nếu host không có Node, hãy quyết định static export từ đầu, đừng để đến ngày deploy.

Khi Next.js hợp lý

Chọn Next.js khi sản phẩm là một tập URL: blog, tài liệu, trang marketing có nhiều đường dẫn, hoặc app mà từng màn hình cần title và mô tả riêng. App Router gom layout, metadata và trang trong một cây thư mục. Với nội dung viết bằng MDX, mỗi bài có thể thành một trang HTML lúc build.

Website BloodyCoTech dùng hướng này. Nội dung là các trang công khai, không có tài khoản, không có form gửi về server. Static export phù hợp hơn một server Next.js thường trực.

Static export đổi những gì

Trong next.config, output: "export" bảo Next.js render mọi route lúc build và ghi HTML, CSS, JS ra thư mục out. Không còn server để chạy Server Action, đọc cookie theo request, hay tối ưu ảnh bằng loader mặc định của next/image. Route động phải được liệt kê bằng generateStaticParams.

Đó là đánh đổi có chủ đích, không phải cấu hình dở. Bạn mất tính năng phụ thuộc request, và đổi lại một website chỉ là file tĩnh, đặt được lên Cloudflare Pages trong hạn mức miễn phí nếu lưu lượng nằm trong quota của họ. Cách đặt build command và thư mục out được viết riêng trong bài triển khai lên Cloudflare Pages.

Nếu sản phẩm sau này cần đăng nhập hoặc dữ liệu theo từng người xem, static export không còn đúng. Lúc đó hãy chuyển mô hình deploy, đừng nhét một API giả vào trang tĩnh.

Cách chọn trong một buổi

  1. Liệt kê URL thật sự cần tồn tại. Nếu chỉ có một URL, bắt đầu bằng React và bundler.
  2. Đánh dấu phần nào cần state và sự kiện. Phần đó là client, dù bạn có dùng Next.js hay không.
  3. Hỏi host cuối cùng có chạy Node hay chỉ phục vụ file. Nếu chỉ phục vụ file, đừng thiết kế Server Action.
  4. Thêm TypeScript trước khi thêm thư viện state. Bài các pattern TypeScript trong React đi tiếp từ đây.

Chọn framework xong vẫn có thể đổi, nhưng đổi sau khi đã viết mười trang tích Server Action thì đắt hơn là chọn đúng từ file cấu hình đầu tiên.

Tài liệu