Mục lục
Việc React bổ sung thêm tính năng Server Components (RSC) trong phiên bản 18 là cú nổ lớn nhất kể từ ngày họ ra mắt React Hooks. Sự kiện này đã định hình lại hoàn toàn cách chúng ta xây dựng ứng dụng web.
Tuy nhiên, sự chuyển đổi này kéo theo một làn sóng hoang mang không hề nhỏ. Tại sao cái gì cũng phải chạy
trên Server? Tại sao useState lại lỗi? Khi nào thì chèn "use client"? Bài viết
này sẽ bóc tách cặn kẽ mọi thứ để bạn không còn sợ hãi trước RSC.
Lịch Sử Từ CSR, SSR Đến RSC
Để hiểu tại sao có RSC, chúng ta phải nhìn lại lịch sử đau thương của Web Performance:
- CSR (Client-Side Rendering): Cách cổ điển của Create React App. Trình duyệt tải về
một file
index.htmltrắng tinh, sau đó phải tải cục Javascript hàng Megabyte, chạy JS, gọi API rồi mới in ra UI. Kết quả: Màn hình trắng dài, SEO bằng 0. - SSR (Server-Side Rendering): Cách cổ điển của Next.js (Page Router). Server xử lý React, in ra cục HTML hoàn chỉnh gửi về trình duyệt. SEO tuyệt vời, hiện hình nhanh. Nhưng... Trình duyệt vẫn phải tải toàn bộ đống Javascript khổng lồ về để "Hydrate" (gắn sự kiện click, gõ phím) thì trang web mới tương tác được.
Và đây là rắc rối lớn nhất của SSR: Dù HTML được render trên Server, nhưng mọi component React vẫn phải gửi qua Internet dưới dạng Javascript cho Client. Nếu ứng dụng có thư viện chuyển đổi Markdown siêu nặng (2MB), người dùng phải tải cả thư viện đó dù họ chỉ vào để đọc chữ!
React Server Components cho phép các component chạy trên Server và ở lại luôn trên Server. Nó chỉ gửi kết quả UI (dạng text đặc biệt) về cho Client, KHÔNG HỀ gửi kèm mã Javascript của component đó.
Mô Hình Tư Duy (Mental Model) Của RSC
Mặc định trong Next.js App Router, mọi component bạn tạo ra đều là Server Component. Hãy ghi nhớ luật vàng sau đây:
"Server Components chạy trên máy chủ (Node.js/Edge). Không có DOM, Không có Trình Duyệt, Không có Tương Tác."
Do đó, trong Server Components, bạn TUYỆT ĐỐI KHÔNG THỂ sử dụng:
useState,useReducer(Vì State là thứ tồn tại trên bộ nhớ trình duyệt).useEffect,useLayoutEffect(Vì nó diễn ra sau khi render lên màn hình DOM).- Các sự kiện tương tác trực tiếp:
onClick,onChange. - Các API của trình duyệt:
window,document,localStorage.
Bù lại, vì nó chạy thẳng trên máy chủ (nằm sát rạt với Database), nên nó có thể làm những thứ Client bó
tay: Truy vấn Database SQL trực tiếp, đọc file hệ thống fs.readFile, hoặc giấu kín các API
Key bảo mật.
Ranh Giới: Bí Ẩn "use client"
Vậy làm sao để làm cái nút có onClick? Bạn phải sử dụng Client Component
bằng cách đặt dòng thần chú "use client" ở ĐẦU file.
"use client"; // Đặt cái này lên đầu trang!
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Bấm tôi: {count}
</button>
);
}
Lưu ý chí mạng: "use client" KHÔNG có nghĩa là Component này "chỉ chạy trên
Client". Nó có nghĩa là Component này sẽ được bao gồm trong gói Javascript gửi về trình duyệt. Thực tế,
Client Component vẫn được render một lần trên Server (để ra được HTML SSR) và sau đó hydrate trên Client.
Data Fetching Cực Nhàn
Trước kia, với Client React, việc gọi API luôn kéo theo ác mộng: useEffect,
loading state, error state, race conditions. Với Server Components,
mọi thứ trở nên sạch sẽ chưa từng có. React Server Component hỗ trợ native async / await!
// Bỏ đi "use client"
import db from '@/lib/db';
export default async function UserProfile({ id }) {
// Chạy thẳng xuống DB lấy data, không cần gọi Fetch API cồng kềnh
const user = await db.user.findUnique({ where: { id } });
if (!user) return <h1>Không tìm thấy user</h1>;
return (
<div>
<h1>Xin chào {user.name}</h1>
<p>Bảo mật: {user.secretCode}</p>
</div>
);
}
Code chạy trên server, vì vậy đoạn Javascript của Prisma/DB Driver và logic bảo mật sẽ không bao giờ lọt xuống trình duyệt.
Nghệ Thuật Lồng Ghép (Interleaving)
Ranh giới nguy hiểm nhất mà mọi Newbie đều sập bẫy: Bạn KHÔNG THỂ import Server Component vào trong Client Component!
Nếu bạn cố tình import Server Component vào một file có dòng "use client", thì Server
Component đó sẽ bị ép biến thành Client Component, và nếu nó có đoạn truy vấn DB, code sẽ
nổ banh xác trên trình duyệt.
Hãy truyền Server Component vào Client Component thông qua prop children.
// ClientWrapper.tsx
"use client";
export default function ClientWrapper({ children }) {
return (
<div onClick={() => alert('Hello')}>
{/* Nó không cần biết children là ai */}
{children}
</div>
);
}
// Page.tsx (Server Component gốc)
import ClientWrapper from './ClientWrapper';
import HeavyServerComponent from './HeavyServer';
export default function Page() {
return (
<ClientWrapper>
{/* Ta truyền SC vào qua children */}
<HeavyServerComponent />
</ClientWrapper>
);
}
Tổng Kết
React Server Components mang lại thiết kế tối ưu nhất cho hiệu năng: Giữ những thứ nặng nề, bảo mật (Database, Data Fetching, Markdown parsing) ở lại Server. Chỉ đẩy xuống Client những Component thực sự cần Tương Tác (Buttons, Forms, Modals).
Bạn không mất đi sức mạnh của React truyền thống, bạn chỉ được cung cấp một công cụ mạnh hơn để dọn dẹp đống rác Javascript khổng lồ đang bủa vây máy tính người dùng mỗi ngày. Chúc bạn code "cháy máy" với App Router!