بازگشت به وبلاگ

Server Component در برابر Client Component: یه مدل ذهنی برای App Router

۲ مرداد ۱۴۰۵3 دقیقه مطالعه
Server Component در برابر Client Component: یه مدل ذهنی برای App Router

رایج‌ترین اشتباه App Router یه باگ نیست — عادت گذاشتن "use client" بالای هر فایل از سر عادتِ دوران Pages Router‌ه. مدل ذهنی‌ای که واقعاً کمک می‌کنه اینه: Server Component پیش‌فرضه، و مرز شبکه، درخت کامپوننت نیست — هرجاییه که خط "use client" رو می‌کشی.

واقعاً چی کجا اجرا می‌شه

  • Server Component ها روی سرور (یا موقع build) رندر می‌شن، هیچ‌وقت جاوااسکریپتشون به مرورگر نمی‌ره، و می‌تونن مستقیم با دیتابیس یا فایل‌سیستم حرف بزنن — بدون نیاز به API route.

  • Client Component ها هم برای HTML اولیه سمت سرور رندر می‌شن، بعد توی مرورگر hydrate می‌شن و اونجا دوباره رندر می‌شن. جاوااسکریپتشون واقعاً به کلاینت می‌ره.

  • توی App Router، همه‌چیز به‌طور پیش‌فرض Server Componentه. "use client" بالای یه فایل، اون فایل و هرچی که import می‌کنه رو جزو باندل کلاینت می‌کنه.

قانون سرانگشتی

از خودت بپرس: این کامپوننت به state، effect، event handler، یا یه API مخصوص مرورگر نیاز داره؟ اگه نه، بذار Server Component بمونه.

// این یه Server Componentه (نیازی به directive نیست) — می‌تونه مستقیم
// یه کوئری دیتابیس رو await کنه، و هیچ‌کدوم از این کد به مرورگر نمی‌ره.
import { prisma } from "@/lib/prisma";

export default async function ProjectList() {
  const projects = await prisma.project.findMany();
  return (
    <ul>
      {projects.map((p) => <li key={p.id}>{p.titleEn}</li>)}
    </ul>
  );
}
"use client";
// این یکی به state و event handler نیاز داره، پس مجبوره Client Component
// باشه — ولی توجه کن کوچیک و متمرکزه، نه کل صفحه.
import { useState } from "react";

export default function LikeButton({ initialCount }: { initialCount: number }) {
  const [count, setCount] = useState(initialCount);
  return <button onClick={() => setCount((c) => c + 1)}>❤ {count}</button>;
}

ترفند ترکیبی که اکثراً از قلم می‌افته

"use client" به این معنی نیست که کل زیردرخت client-rendered می‌شه — یه Client Component همچنان می‌تونه Server Component هایی که به‌عنوان children/prop بهش پاس داده شدن رو رندر کنه. این همون الگوییه که بیشتر صفحه‌ت رو server-rendered نگه می‌داره حتی وقتی به یه جزیره‌ی تعاملی نیاز داری.

"use client";
export function Modal({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false);
  return open ? <div className="modal">{children}</div> : null;
}

// داخل یه Server Component:
import { Modal } from "./modal";
import ServerRenderedComments from "./comments"; // همچنان Server Componentه

export default function Page() {
  return (
    <Modal>
      <ServerRenderedComments /> {/* سمت سرور رندر شده، به‌عنوان child پاس داده شده */}
    </Modal>
  );
}

اشتباه‌های رایج

  • یه layout یا page رو "use client" کردن فقط چون یه دکمه‌ی توش به onClick نیاز داره — به‌جاش خودِ دکمه رو استخراج کن.

  • گرفتن داده توی یه Client Component با useEffect وقتی یه Server Component می‌تونست مستقیم await‌ش کنه — کندتره (waterfall بعد از hydration) و کد بیشتری هم داره.

  • پاس دادن prop غیرقابل‌سریالایز (تابع، نمونه‌ی کلاس) از یه Server به یه Client Component — فقط داده‌ی قابل‌سریالایز از این مرز رد می‌شه.

  • فراموش کردن اینکه هرچی که یه فایل "use client" import می‌کنه هم برای کلاینت باندل می‌شه، حتی اگه اون import خاص از هیچ API مخصوص کلاینتی استفاده نکنه.

"use client" رو تا جایی که می‌شه پایین درخت هل بده. هدف صفر Client Component نیست — کوچیک‌ترین Client Component ممکنه، احاطه‌شده با بقیه‌ی چیزهایی که server-rendered ان.