Server Components و کاربرد آن‌ها

در درس «مدل کامپوننت‌ها و رندر در App Router» نقشه‌ی کلی را دیدیم: کامپوننت‌ها به‌صورت پیش‌فرض Server Component هستند. حالا از نزدیک می‌بینیم Server Component دقیقاً چه کارهایی می‌تواند انجام دهد، چه کارهایی نمی‌تواند و کِی انتخاب درست است. سپس یک بخش «تازه‌ترین نوشته‌ها» را به صفحه‌ی اصلی DevNotes اضافه می‌کنیم که کاملاً با Server Component ساخته می‌شود.

Server Component چیست؟

Server Component یک کامپوننت React معمولی است که فقط روی سرور اجرا می‌شود. نتیجه‌ی رندر آن (نه کد آن) به مرورگر فرستاده می‌شود. برای ساختن آن کار خاصی نمی‌کنید: هر کامپوننتی در پوشه‌ی app یا هر کامپوننتی که فقط از Server Componentها import شده باشد، تا وقتی دستور "use client" نداشته باشد، Server Component است.

اجرای آن در یکی از دو زمان رخ می‌دهد: هنگام Build (برای صفحات از پیش‌ساخته) یا هنگام درخواست. هر بار که اجرا می‌شود، یک‌بار رندر می‌شود؛ برخلاف Client Component، هیچ State یا Effectی ندارد که باعث رندر دوباره‌ی آن در مرورگر شود.

چه کارهایی را می‌توان در Server Component انجام داد؟

۱. دریافت داده با async و await

کامپوننت می‌تواند async باشد و داده را مستقیماً داخل خودش بخواند. همین الگو را در صفحه‌ی فهرست نوشته‌ها داریم:

tsx
export default async function PostsPage() {
  const { posts } = await getPosts({ page: 1 });

  return <p>{posts.length} نوشته</p>;
}

نیازی به useEffect، useState و مدیریت حالت loading نیست. خود نتیجه‌ی آماده با HTML می‌آید.

۲. دسترسی مستقیم به منابع پشت‌صحنه

چون کد فقط روی سرور اجرا می‌شود، Server Component می‌تواند پایگاه داده، فایل‌سیستم یا سرویس‌های داخلی را مستقیماً صدا بزند. در فصل «پایگاه داده با Prisma و SQLite» همین کار را با Prisma انجام می‌دهیم و صفحه‌ها بدون API واسطه از پایگاه داده می‌خوانند.

۳. استفاده‌ی امن از رازها

کلیدهای API، رمز پایگاه داده و متغیرهای محیطی محرمانه فقط در سرور در دسترس‌اند و کدشان به مرورگر نمی‌رسد. با این حال باید مراقب باشید این مقادیر از طریق Props به Client Component منتقل نشوند. روش‌های محافظت را در درس‌های «server-only، Context و اشتباهات رایج» و «متغیرهای عمومی و محرمانه» می‌بینیم.

۴. کاهش JavaScript سمت کلاینت

اگر یک کامپوننت برای ساختن خروجی‌اش از کتابخانه‌ی سنگینی استفاده کند (مثلاً تبدیل Markdown به HTML)، آن کتابخانه در Server Component روی سرور می‌ماند و فقط خروجی HTML به مرورگر می‌رسد. در Client Component همان کتابخانه در بسته‌ی JavaScript کاربر قرار می‌گیرد.

۵. رندر محتوای ثابت

بخش‌هایی که تعاملی نیستند، مثل هدر، فوتر، کارت نوشته، متن مقاله و صفحه‌بندی، نیازی به مرورگر ندارند و بهتر است Server Component بمانند.

چه کارهایی را نمی‌توان در Server Component انجام داد؟

ممنوع در Server Component دلیل راه‌حل
useState، useReducer State در مرورگر و بین رندرها نگه داشته می‌شود؛ Server Component یک‌بار رندر می‌شود بخش تعاملی را به Client Component ببرید
useEffect، useLayoutEffect Effect بعد از رندر در مرورگر اجرا می‌شود Client Component
رویدادها مثل onClick و onChange تابع رویداد در مرورگر اجرا می‌شود و نمی‌توان آن را از سرور فرستاد Client Component
APIهای مرورگر: window، document، localStorage روی سرور وجود ندارند Client Component
Hookهایی که به مرورگر وابسته‌اند: useRouter، usePathname وضعیت مسیر سمت کلاینت را می‌خوانند Client Component (مثل NavLink)
ساخت و مصرف Context با createContext و useContext Context به State و رندر سمت کلاینت وابسته است Client Component (جزئیات در درس «server-only، Context و اشتباهات رایج»)

اگر یکی از این‌ها را در Server Component استفاده کنید، Next.js در حالت توسعه خطایی با پیامی شبیه «این Hook فقط در Client Component کار می‌کند» نشان می‌دهد و راهنمایی می‌کند که "use client" اضافه کنید. این پیام نشانه‌ی خرابی برنامه نیست؛ فقط می‌گوید آن بخش باید به Client Component منتقل شود.

داده‌هایی که از Server به Client می‌روند

وقتی یک Server Component، Client Component را رندر می‌کند و Props می‌دهد، آن Props باید به شکلی قابل ارسال (Serializable) باشند: رشته، عدد، Boolean، آرایه و آبجکت ساده، null و undefined مجازند. تابع، کلاس و مقادیری مثل Symbol مجاز نیستند. جزئیات در درس «Props، انتقال داده و مرز Server/Client» می‌آید.

عملی: بخش «تازه‌ترین نوشته‌ها» در صفحه‌ی اصلی

می‌خواهیم زیر معرفی DevNotes سه نوشته‌ی جدید را نشان بدهیم. کارت نوشته (PostCard) از قبل داریم، ولی در پوشه‌ی خصوصی posts/_components است. حالا دو صفحه از آن استفاده می‌کنند و طبق قاعده‌ای که در درس «پوشه‌های خصوصی و ساختاردهی فایل‌ها» دیدیم، کدِ مشترک بین چند مسیر باید در components قرار بگیرد.

فایل‌های متأثر:

  • app/(public)/posts/_components/PostCard.tsx ← منتقل می‌شود به components/PostCard.tsx (با یک prop جدید)
  • app/(public)/posts/page.tsx (فقط مسیر import عوض می‌شود)
  • components/LatestPosts.tsx (جدید)
  • app/(public)/page.tsx (صفحه‌ی اصلی)

گام ۱: انتقال PostCard و افزودن سطح تیتر

فایل را به components/PostCard.tsx منتقل کنید و آن را به این شکل به‌روز کنید. در صفحه‌ی فهرست، عنوان اصلی صفحه h1 است و کارت‌ها h2 می‌گیرند. اما در صفحه‌ی اصلی بخش «تازه‌ترین نوشته‌ها» خودش یک h2 دارد و عنوان کارت‌ها باید h3 باشد تا سلسله‌مراتب تیترها درست بماند. برای همین سطح تیتر را از بیرون می‌گیریم:

tsx
// components/PostCard.tsx
import Link from "next/link";
import type { Post } from "@/lib/posts";
import { formatDate } from "@/lib/format";

type PostCardProps = {
  post: Post;
  headingLevel?: "h2" | "h3";
};

export default function PostCard({
  post,
  headingLevel: Heading = "h2",
}: PostCardProps) {
  return (
    <article>
      <Heading>
        <Link href={`/posts/${post.slug}`}>{post.title}</Link>
      </Heading>
      <p>{post.excerpt}</p>
      <time dateTime={post.publishedAt}>
        {formatDate(post.publishedAt)}
      </time>
    </article>
  );
}
  • headingLevel: Heading = "h2" prop را دریافت می‌کند، با نام Heading (با حرف بزرگ) در دسترس قرار می‌دهد و مقدار پیش‌فرض را h2 می‌گذارد.
  • در JSX، نامی که با حرف بزرگ شروع شود به‌عنوان کامپوننت یا مقدار متغیر خوانده می‌شود؛ پس <Heading> همان تگ h2 یا h3 را می‌سازد.
  • هیچ "use client"ی لازم نیست؛ این یک Server Component ساده است.

گام ۲: به‌روزرسانی import در صفحه‌ی فهرست

در app/(public)/posts/page.tsx فقط خط import کارت را عوض کنید و بقیه‌ی فایل دست‌نخورده می‌ماند:

tsx
// app/(public)/posts/page.tsx (فقط خط import)
// قبل:
// import PostCard from "./_components/PostCard";
// بعد:
import PostCard from "@/components/PostCard";

گام ۳: کامپوننت LatestPosts

tsx
// components/LatestPosts.tsx
import PostCard from "@/components/PostCard";
import { getAllPosts } from "@/lib/posts";

type LatestPostsProps = {
  count?: number;
};

export default async function LatestPosts({ count = 3 }: LatestPostsProps) {
  const posts = await getAllPosts();
  const latest = posts.slice(0, count);

  if (latest.length === 0) {
    return <p>هنوز نوشته‌ای منتشر نشده است.</p>;
  }

  return (
    <div>
      {latest.map((post) => (
        <PostCard key={post.slug} post={post} headingLevel="h3" />
      ))}
    </div>
  );
}
  • تابع async است و داده را مستقیماً داخل کامپوننت می‌خواند؛ همان الگوی Server Component.
  • getAllPosts نوشته‌ها را از جدیدترین به قدیمی‌ترین برمی‌گرداند (در درس «ساخت فهرست و صفحه‌ی جزئیات نوشته‌ها» مرتب کردیم). با slice(0, count) فقط چند تای اول را می‌گیریم.
  • برای حالت خالی هم یک پیام در نظر گرفته‌ایم تا خروجی بی‌معنی نشود.
  • این کامپوننت هیچ State و رویدادی ندارد، پس Server Component بودنش درست و کم‌هزینه است.

گام ۴: صفحه‌ی اصلی

tsx
// app/(public)/page.tsx
import Link from "next/link";
import LatestPosts from "@/components/LatestPosts";

export default function HomePage() {
  return (
    <main>
      <h1>DevNotes</h1>
      <p>
        وب‌سایتی برای انتشار و مدیریت نوشته‌های فنی. در طول دوره‌ی
        Next.js 16 این پروژه را قدم‌به‌قدم کامل می‌کنیم.
      </p>

      <section aria-labelledby="latest-posts-heading">
        <h2 id="latest-posts-heading">تازه‌ترین نوشته‌ها</h2>
        <LatestPosts />
        <p>
          <Link href="/posts">مشاهده‌ی همه‌ی نوشته‌ها</Link>
        </p>
      </section>
    </main>
  );
}

HomePage خودش async نیست، چون داده‌ای نمی‌خواند؛ فقط یک کامپوننت async (یعنی LatestPosts) را رندر می‌کند و این کاملاً مجاز است. هر Server Component می‌تواند داده‌ی مورد نیازش را خودش بگیرد و صفحه فقط آن‌ها را کنار هم می‌گذارد.

آزمایش: اثبات اجرا فقط روی سرور

  1. داخل LatestPosts، اولین خط تابع، console.log("LatestPosts روی سرور اجرا شد"); را اضافه کنید.
  2. صفحه‌ی http://localhost:3000 را ریفرش کنید.
  3. پیام را در ترمینال ببینید و بعد کنسول مرورگر (Console) را بررسی کنید.

پیام فقط در ترمینال ظاهر می‌شود. در مرورگر این کد هرگز اجرا نشده است. می‌توانید با «View Page Source» ببینید عنوان نوشته‌ها داخل HTML اولیه هست، یعنی خروجی آن‌ها از سرور آمده است.

حالا یک آزمایش دوم: یک useState فرضی در LatestPosts اضافه کنید (با import از react) و صفحه را ریفرش کنید. Next.js خطایی نشان می‌دهد که به‌خاطر Hook در Server Component است. بعد از دیدن پیام، useState و هر دو console.log را پاک کنید تا پروژه تمیز بماند.

خروجی و رفتار مورد انتظار

  • در / زیر معرفی، عنوان «تازه‌ترین نوشته‌ها» و سه کارت از جدیدترین نوشته‌ها (با عنوان h3) دیده می‌شود.
  • در /posts کارت‌ها مثل قبل نمایش داده می‌شوند و عنوانشان h2 است.
  • هر دو صفحه از یک PostCard مشترک استفاده می‌کنند.
  • هیچ کدی از LatestPosts یا PostCard به مرورگر فرستاده نمی‌شود.

کِی Server Component را انتخاب کنیم؟

نیاز انتخاب
خواندن داده از پایگاه داده یا API Server Component
استفاده از کلید یا رمز محرمانه Server Component
نمایش محتوای ثابت و فهرست‌ها Server Component
کتابخانه‌ی سنگین فقط برای ساختن خروجی Server Component
State، رویداد کلیک یا تایپ Client Component
APIهای مرورگر یا Hookهای مسیر Client Component

راهنمای کامل‌تر انتخاب را در درس «انتخاب محل اجرای کد و کاهش JavaScript سمت کلاینت» می‌بینیم.

نکته‌های مهم

  • Server Component به معنی «اجرا فقط یک‌بار» نیست. هر بار که صفحه‌ی پویا درخواست شود (یا بعد از router.refresh() یا بازاعتبارسنجی)، دوباره اجرا می‌شود. این رفتار با کش و نوع رندر ارتباط دارد و در فصل‌های «Rendering و Data Fetching» و «Cache و Revalidation در Next.js 16» بررسی می‌شود.
  • خطای async در Client Component: تابع کامپوننت فقط در Server Component می‌تواند async باشد. اگر "use client" بگذارید و تابع را async بنویسید، React خطا می‌دهد.
  • دسترسی به داده را مستقیم در Props منتقل نکنید. اگر آبجکت کاملی مثل رکورد کاربر را به Client Component بدهید، همه‌ی فیلدهایش (شاید شامل چیزهای حساس) به مرورگر می‌رسد. فقط همان فیلدهایی را بدهید که لازم است.
  • بازسازی بجا: وقتی کامپوننتی بین چند مسیر مشترک شد، به‌جای کپی‌کردن آن را به components منتقل کنید، همان‌طور که با PostCard کردیم.

جمع‌بندی

  • Server Component پیش‌فرض است، فقط روی سرور اجرا می‌شود و کدش به مرورگر نمی‌رسد.
  • می‌تواند async باشد، داده را مستقیم بخواند، به منابع و رازهای سرور دسترسی داشته باشد و حجم JavaScript را کم کند.
  • نمی‌تواند State، Effect، رویداد مرورگر، APIهای مرورگر، Hookهای مسیر یا Context استفاده کند.
  • Propsی که به Client Component می‌دهیم باید Serializable باشند.
  • در DevNotes اکنون LatestPosts و PostCard نمونه‌ی خوبی از Server Componentهای مشترک هستند و صفحه‌ی اصلی تازه‌ترین نوشته‌ها را نشان می‌دهد.
  • در درس بعد، «Client Components و دستور use client»، می‌بینیم چه زمانی و چگونه یک کامپوننت را به Client Component تبدیل کنیم.