ข้ามไปยังเนื้อหา

Server และ Client Components

ใน App Router ทุก component เป็น Server Component จนกว่าไฟล์จะบอกว่า "use client" และบรรทัดเดียวนี้ลากเส้น boundary ที่ตัดสินว่าโค้ดไหนจะไปถึง browser

component ใน app/ รันบน server เว้นแต่คุณจะ opt out นั่นแปลว่าเป็น async ได้, await data ได้ และแตะ database, filesystem หรือ secret ตรง ๆ ได้ — โค้ดพวกนี้ไม่ถูกส่งไป browser เลย

// app/dashboard/page.tsx — a Server Component (no directive needed)
import { db } from '@/lib/db';
export default async function Dashboard() {
const users = await db.user.findMany(); // runs on the server only
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

เพราะสิ่งนี้ไม่เคยถูกส่งไป client browser จึงไม่ต้องดาวน์โหลด JavaScript ของ component พวกนี้เลยสักไบต์ Server Components ใช้ useState, useEffect, event handler หรือ browser API ไม่ได้ — เพราะ render ครั้งเดียวบน server แล้วจบ

วาง "use client" ไว้บนสุดของไฟล์ แล้วทุกอย่างที่ไฟล์นั้น export จะกลายเป็น Client Component ซึ่งรันใน browser และใช้ hook, state, effect, event handler และ browser API ได้

'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

จุดที่ละเอียดอ่อนคือ boundary นี้เป็น transitive ทุก module ที่ Client Component import เข้ามาจะถูกดึงเข้า client bundle ด้วย ไม่ว่าจะมี directive ของตัวเองหรือไม่ คุณมาร์ก boundary ครั้งเดียวที่บนสุดของ tree — ไม่ต้องใส่ "use client" ซ้ำในทุก child

มีสองกฎที่คุมว่าทั้งสองชนิดผสมกันยังไง และไม่สมมาตร —

  • Server Component import และ render Client Component ได้ตรง ๆ
  • Client Component import Server Component ไม่ได้ — แต่รับเข้ามาเป็น children หรือ prop ได้

กฎข้อที่สองคือ doughnut (slot) pattern — Client Component เป็นวงแหวน และ Server Component เติมเข้าไปในรู เนื้อหาที่ render บน server ยังอยู่บน server แม้จะมี client component ล้อมรอบ

// app/page.tsx — Server Component
import ClientShell from './client-shell';
import ServerData from './server-data';
export default function Page() {
return (
<ClientShell>
<ServerData /> {/* stays a Server Component, passed as children */}
</ClientShell>
);
}

มีข้อจำกัดหนึ่งที่ทำให้สิ่งนี้ทำงานได้ — prop ที่ข้าม boundary ต้อง serializable คุณส่ง string, number, plain object, array และ JSX ได้ แต่ส่ง function, class instance หรือ Date เป็น prop ดิบ ๆ ไม่ได้ — ค่าพวกนี้รอดการเดินทางจาก server ไป client ไม่ได้

flowchart LR
  Root[Server Component root] --> Client["use client boundary"]
  Root --> Server[More Server Components]
  Client --> Bundle[Imports join client bundle]
  Root -. passes children .-> Client
  Client -. renders server child .-> Server
Where the client boundary sits
ใน App Router component เป็นอะไรก่อนที่จะเพิ่ม directive ใด ๆ
"use client" ทำอะไรกับ module ที่ไฟล์นั้น import เข้ามา
Client Component จะรวมเนื้อหาแบบ Server Component ได้ยังไง
prop ไหนข้าม boundary จาก server ไป client ได้อย่างปลอดภัย