Bỏ qua đến nội dung chính
ReactServer ComponentsNext.jsSSRfrontend

React Server Components Là Gì? Khác Client Components

React Server Components là gì? Render trên server, không gửi JS, fetch data trực tiếp. So sánh với Client Components, khi nào dùng cái nào trong Next.js.

Xuất bản 8 phút đọc

React Server Components (RSC) là thay đổi lớn nhất React kể từ hooks. Bài này giải thích RSC từ đầu, code Next.js App Router minh hoạ — và quan trọng nhất: khi nào dùng server, khi nào client.

Vấn đề RSC giải

App SPA truyền thống: server gửi HTML rỗng + JS bundle 500KB. Browser tải, parse, execute, fetch data, render. Time to Interactive 3-5 giây trên 3G.

SSR truyền thống: server render HTML, gửi cùng JS bundle. HTML hiện nhanh nhưng vẫn phải hydrate — JS download xong mới interactive.

RSC: render trên server thành "RSC payload" (không phải HTML thuần). Component không cần interactivity → KHÔNG gửi JS xuống client. Bundle giảm 30-70% với app điển hình.

Mặc định: server component

// app/blog/page.tsx — Server Component (default trong Next App Router)
import { db } from '@/lib/db'

export default async function BlogPage() {
  const posts = await db.posts.findAll()  // ← fetch trực tiếp, không cần API

  return (
    <div>
      <h1>Blog</h1>
      {posts.map(p => (
        <article key={p.id}>
          <h2>{p.title}</h2>
          <p>{p.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

Component này:

  • Chạy trên server, fetch DB trực tiếp
  • Render thành RSC payload (binary serialized)
  • Browser nhận, render — KHÔNG download JS của component
  • Chỉ tốn JS cho Next.js runtime (~30KB) + client component nếu có

Khi nào "use client"?

// components/like-button.tsx
'use client'

import { useState } from 'react'

export default function LikeButton({ postId }: { postId: number }) {
  const [liked, setLiked] = useState(false)

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'} {liked ? 'Liked' : 'Like'}
    </button>
  )
}

Cần "use client" khi component dùng:

  • Hook: useState, useEffect, useReducer, useRef
  • Event handler: onClick, onChange, onSubmit
  • Browser API: window, localStorage, navigator
  • Class component (legacy)
  • React Context Provider

Composition: server import client

// app/blog/[slug]/page.tsx — Server Component
import LikeButton from '@/components/like-button'  // client component
import { db } from '@/lib/db'

export default async function PostPage({ params }) {
  const post = await db.posts.findBySlug(params.slug)

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <LikeButton postId={post.id} />  {/* client island trong server tree */}
    </article>
  )
}

Pattern: phần lớn page là server (nội dung), interactive bit là client island. Bundle JS chỉ cho island — toàn bộ render content không cost client JS.

Streaming + Suspense

Server component có thể await — nhưng đừng để 1 query chậm block toàn page. Suspense + streaming:

import { Suspense } from 'react'

export default function Dashboard() {
  return (
    <div>
      <Header />  {/* render ngay */}

      <Suspense fallback={<Spinner />}>
        <UserProfile />  {/* fetch chậm — fallback hiện trước */}
      </Suspense>

      <Suspense fallback={<Spinner />}>
        <RecentOrders />
      </Suspense>
    </div>
  )
}

async function UserProfile() {
  const user = await fetch('https://slow-api...').then(r => r.json())
  return <div>{user.name}</div>
}

Page render header ngay. UserProfile và RecentOrders stream xuống khi sẵn sàng. User thấy progressive load thay vì màn trắng.

Server Actions — mutation

// actions.ts
'use server'

import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  await db.posts.insert({ title, content })
  revalidatePath('/blog')  // invalidate cache
}
// form.tsx — client component dùng action
'use client'
import { createPost } from './actions'

export default function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" />
      <textarea name="content" />
      <button>Tạo</button>
    </form>
  )
}

Form submit gọi function server trực tiếp — không cần API endpoint riêng. Validation, auth check làm trong action.

Data fetching pattern mới

// ❌ Cũ: client fetch qua /api/posts
'use client'
function Posts() {
  const [posts, setPosts] = useState([])
  useEffect(() => { fetch('/api/posts').then(r => r.json()).then(setPosts) }, [])
  // ...
}

// ✓ Mới: server component fetch trực tiếp
async function Posts() {
  const posts = await db.posts.findAll()
  return <ul>{posts.map(p => <li>{p.title}</li>)}</ul>
}

Lợi: không có loading state, không có error handling client, SQL query có thể optimized nhờ chạy gần DB, secret không expose client.

3 pitfall thường gặp

  • "use client" toàn page: mất hoàn toàn benefit RSC. Đẩy "use client" xuống component nhỏ nhất cần.
  • Pass non-serializable prop: server không thể truyền function/Date object xuống client component. Phải JSON-able.
  • Quên revalidate: server action mutate data nhưng cache vẫn cũ → user thấy stale. revalidatePath() hoặc revalidateTag().

Kết luận

React Server Components là tương lai của React framework. Mặc định server, "use client" cho island interactive. Bundle giảm, fetch nhanh, secret an toàn. Nếu đang start project mới, dùng Next.js App Router với RSC native — đừng cố stick với Pages Router. Tham khảo Migrate App Router.

Zalo