Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

Next.js 15 và React 19: khi nào nên dùng Server Components, Server Actions và render phía server

Ếch Trendy
Ếch Trendy

3 tháng trước · 10 phút đọc

Server Components là gì và tại sao React 19 làm nó quan trọng hơn

Trước React 18, mọi component đều chạy trên browser. Bạn load trang, JavaScript tải xuống, React khởi động, component render, rồi data mới được fetch. Chuỗi này tốn thời gian, và bundle size phình to theo từng dependency bạn thêm vào.

React 19 ổn định hóa Server Components sau nhiều năm thử nghiệm. Next.js 15 xây dựng hoàn toàn trên nền này. Điểm khác biệt cốt lõi: Server Components chạy trên server, không bao giờ xuất hiện trong JavaScript bundle gửi về browser.

344970 Server render HTML sẵn - browser nhận trang đã có nội dung

Điều này có nghĩa gì trong thực tế? Giả sử bạn có một blog dùng marked để render Markdown. Với Client Component, toàn bộ thư viện marked (~50KB) được gửi xuống browser. Với Server Component, marked chạy trên server, browser chỉ nhận HTML đã render sẵn. Bundle size về 0 cho dependency đó.

Next.js 15 mặc định mọi component trong thư mục app/ đều là Server Component. Bạn phải khai báo rõ 'use client' ở đầu file nếu muốn component đó chạy trên browser.

// Server Component - mặc định trong Next.js 15
// Không cần khai báo gì thêm
async function BlogPost({ id }: { id: string }) {
  // Fetch thẳng trong component, không cần useEffect
  const post = await db.posts.findById(id);
  
  return <article>{post.content}</article>;
}

// Client Component - phải khai báo rõ
'use client';
import { useState } from 'react';

function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>Like</button>;
}

Mình nhớ lần đầu chuyển một project từ Pages Router sang App Router, cảm giác lạ nhất là fetch data thẳng trong component mà không cần useEffect hay getServerSideProps. Nó đơn giản đến mức khó tin là đúng.


Khi nào dùng Server Components, khi nào dùng Client Components

Câu hỏi này gây nhầm lẫn nhiều nhất khi mới bắt đầu với App Router. Không có công thức cứng nhắc, nhưng có một mental model đơn giản: nếu component cần tương tác hoặc state, dùng Client. Còn lại, mặc định Server.

344971 Chọn đúng loại component từ đầu tiết kiệm nhiều refactor về sau

Cụ thể hơn:

Dùng Server Component khi:

  • Fetch data từ database hoặc API (không cần loading state phức tạp)
  • Render nội dung tĩnh hoặc bán tĩnh (blog, landing page, product listing)
  • Dùng các thư viện nặng chỉ để xử lý data (markdown parser, date formatter)
  • Cần truy cập server-only resources (file system, secrets, internal APIs)

Dùng Client Component khi:

  • Dùng useState, useEffect, hoặc bất kỳ hook nào
  • Cần event listeners (onClick, onChange, onSubmit)
  • Dùng browser APIs (localStorage, geolocation, window)
  • Cần real-time updates hoặc WebSocket

Một pattern hay trong thực tế là composition: giữ Server Component bao ngoài, nhúng Client Component nhỏ bên trong đúng chỗ cần.

// Server Component bao ngoài - fetch data, render layout
async function ProductPage({ id }: { id: string }) {
  const product = await fetchProduct(id);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {/* Chỉ phần này cần client */}
      <AddToCartButton productId={id} price={product.price} />
    </div>
  );
}

// Client Component nhỏ - chỉ xử lý interaction
'use client';
function AddToCartButton({ productId, price }: Props) {
  const [loading, setLoading] = useState(false);
  
  async function handleAddToCart() {
    setLoading(true);
    await addToCart(productId);
    setLoading(false);
  }
  
  return (
    <button onClick={handleAddToCart} disabled={loading}>
      {loading ? 'Đang thêm...' : `Thêm vào giỏ - ${price}đ`}
    </button>
  );
}

Tư duy đúng: đẩy 'use client' xuống thấp nhất có thể trong cây component. Càng ít component phải chạy trên browser, bundle size càng nhỏ, First Load JS càng thấp.


Server Actions: submit form và mutate data không cần API endpoint

Đây là phần mình thấy thú vị nhất trong Next.js 15. Server Actions cho phép bạn định nghĩa một hàm chạy trên server, rồi gọi nó thẳng từ component như một hàm bình thường, kể cả từ Client Component.

Nghe có vẻ lạ, nhưng đây là cách nó hoạt động: Next.js tự tạo một API endpoint ẩn, serialize arguments, gửi request, rồi trả kết quả về. Bạn không cần viết /api/submit-form nữa.

// actions.ts - khai báo Server Action
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export async function createComment(formData: FormData) {
  const content = formData.get('content') as string;
  const postId = formData.get('postId') as string;
  
  // Validate và lưu thẳng vào DB
  if (!content.trim()) {
    return { error: 'Nội dung không được để trống' };
  }
  
  await db.comments.create({ content, postId });
  
  // Xóa cache để trang tự refresh data mới
  revalidatePath(`/posts/${postId}`);
  return { success: true };
}

344972 Server Action loại bỏ hẳn lớp API route cho các mutation đơn giản

// CommentForm.tsx - Client Component dùng Server Action
'use client';
import { createComment } from './actions';
import { useActionState } from 'react';

export function CommentForm({ postId }: { postId: string }) {
  const [state, formAction, isPending] = useActionState(createComment, null);
  
  return (
    <form action={formAction}>
      <input type="hidden" name="postId" value={postId} />
      <textarea name="content" placeholder="Viết bình luận..." />
      {state?.error && <p className="text-red-500">{state.error}</p>}
      <button type="submit" disabled={isPending}>
        {isPending ? 'Đang gửi...' : 'Gửi bình luận'}
      </button>
    </form>
  );
}

useActionState là hook mới trong React 19, thay thế useFormState cũ. Nó handle loading state, error state và kết quả trả về từ Server Action trong một hook duy nhất.

Lợi ích rõ nhất: giảm boilerplate đáng kể. Không cần viết fetch trong client, không cần tạo API route riêng, không cần quản lý loading state thủ công. Và vì logic chạy trên server, bạn có thể truy cập DB trực tiếp, không cần lo lộ credentials.

Một điểm quan trọng cần nhớ: Server Actions vẫn là HTTP request thật. Progressive enhancement cũng hoạt động - nếu JavaScript bị disabled, form HTML thuần vẫn submit được vì action của form trỏ đến endpoint thật.


Static, Dynamic và Streaming: 3 kiểu render trong Next.js 15

Next.js 15 không chỉ có một kiểu render. Hiểu được 3 kiểu này giúp bạn tối ưu đúng chỗ thay vì áp dụng một công thức cho tất cả.

Static rendering (mặc định): Page được render lúc build time hoặc sau khi revalidate. Phù hợp cho nội dung ít thay đổi như blog, trang marketing, documentation. Next.js 15 tích cực cache hơn Next.js 14 - mặc định fetch không cache nữa (breaking change đáng chú ý).

Dynamic rendering: Page render mỗi request. Tự động kích hoạt khi bạn dùng cookies(), headers(), hay searchParams. Phù hợp cho dashboard, trang cá nhân hóa, giỏ hàng.

Streaming: Gửi HTML từng phần về browser trong khi server vẫn đang xử lý. Dùng <Suspense> để bao quanh component chậm.

344973 Streaming giúp người dùng thấy nội dung sớm thay vì chờ toàn trang xong

// app/dashboard/page.tsx - kết hợp Static và Streaming
import { Suspense } from 'react';

// Layout tĩnh render ngay
export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <nav>...</nav>
      
      {/* Phần chậm - stream sau */}
      <Suspense fallback={<SkeletonCard />}>
        <RecentOrders />
      </Suspense>
      
      {/* Phần rất chậm - stream riêng, không block nhau */}
      <Suspense fallback={<SkeletonChart />}>
        <RevenueChart />
      </Suspense>
    </div>
  );
}

// Component này fetch data riêng, stream độc lập
async function RecentOrders() {
  const orders = await db.orders.findRecent(); // Giả sử mất 800ms
  return <OrderList orders={orders} />;
}

async function RevenueChart() {
  const data = await analytics.getRevenue(); // Giả sử mất 1.2s
  return <Chart data={data} />;
}

Với cách trên, browser nhận ngay phần header và nav. RecentOrders xong trước thì stream trước (800ms), RevenueChart stream sau (1.2s). Nếu không dùng Streaming, cả trang phải chờ 1.2s mới có gì để hiển thị.

Breaking change quan trọng trong Next.js 15: fetch requests không còn cached mặc định. Nếu bạn muốn cache, phải khai báo rõ:

// Next.js 15 - phải khai báo cache rõ ràng
const data = await fetch('/api/posts', {
  next: { revalidate: 3600 } // Cache 1 giờ
});

// Hoặc force cache
const data = await fetch('/api/posts', {
  cache: 'force-cache'
});

Nếu bạn đang nâng cấp từ Next.js 14, đây là thứ dễ break nhất. Kiểm tra lại toàn bộ fetch calls trước khi deploy.


Trade-off thực tế và khi nào không nên dùng Server Components

Server Components không phải silver bullet. Có những tình huống bạn nên cân nhắc kỹ trước khi dùng.

Debugging phức tạp hơn: Lỗi xảy ra trên server, log xuất hiện ở terminal, không phải DevTools của browser. Khi component vừa có phần server vừa có phần client, trace lỗi đòi hỏi quen hơn với mental model mới.

Không phù hợp với real-time data: Nếu bạn build chat app, live dashboard cập nhật mỗi giây, hay bất cứ thứ gì cần WebSocket - Server Components không giúp được. Bạn vẫn cần Client Component với SWR, React Query, hoặc WebSocket.

Vendor lock-in một phần: App Router của Next.js implement Server Components theo cách riêng. Migrate sang framework khác sau này sẽ tốn công hơn so với dùng Pages Router thuần.

344974 Chọn đúng tool cho đúng use case, không phải cứ mới là dùng

Khi nào vẫn nên dùng Pages Router (hoặc Vite + React):

  • Dự án nhỏ, team chưa quen App Router
  • App có nhiều real-time features từ đầu
  • Cần migrate nhanh, không có thời gian học mental model mới
  • SPA thuần không cần SEO (internal tools, admin panel)

Mình thấy một pattern phổ biến là over-engineering: chuyển toàn bộ codebase sang App Router rồi nhận ra 80% components vẫn phải là Client Components vì logic business phức tạp. Kết quả? Code khó đọc hơn, không benefit gì thêm.

Quy tắc thực tế: nếu page của bạn cần SEO và data phần lớn là static hoặc server-fetched, Server Components sẽ giúp nhiều. Nếu app là SPA nặng với nhiều interaction, lợi ích giảm đi đáng kể.

Nếu bạn muốn nắm vững React trước khi bước vào Next.js 15, khóa Xây Dựng Website với ReactJS trên F8 cover từ cơ bản đến nâng cao - đủ nền để không bị ngợp khi gặp App Router lần đầu.


Tổng kết và bước tiếp theo

Ba thứ cần nhớ từ bài này:

  • Server Components giảm bundle size và đơn giản hóa data fetching - dùng mặc định, chỉ opt-in 'use client' khi thật sự cần.
  • Server Actions loại bỏ API routes cho các mutation đơn giản - phù hợp cho forms, CRUD operations, không phù hợp cho logic business phức tạp cần nhiều lớp validation.
  • Streaming với Suspense cải thiện perceived performance rõ nhất trên các page có nhiều data sources chậm độc lập nhau.

Next.js 15 và React 19 không thay đổi cách bạn build UI - chúng thay đổi ở đâu logic chạy. Mental model này mất vài ngày để quen, nhưng khi quen rồi thì code sạch và nhanh hơn đáng kể.

Bước tiếp theo mình gợi ý:

  1. Tạo một project App Router mới, thử fetch data thẳng trong Server Component xem cảm giác thế nào
  2. Viết một Server Action đơn giản cho form contact hoặc comment
  3. Thêm <Suspense> vào một component chậm và so sánh LCP trước/sau

Nếu bạn đang bắt đầu từ đầu với React, nền tảng vững thì việc tiếp cận Next.js 15 sẽ dễ hơn nhiều. Khóa Xây Dựng Website với ReactJS của F8 là điểm khởi đầu tốt - miễn phí, có bài tập thực hành.

Gặp vướng mắc chỗ nào, drop comment mình xem cùng.

344975 Bắt đầu nhỏ, hiểu rõ từng phần trước khi migrate cả project