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 باشد و داده را مستقیماً داخل خودش بخواند. همین الگو را در صفحهی فهرست نوشتهها داریم:
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 باشد تا سلسلهمراتب تیترها درست بماند. برای همین سطح تیتر را از بیرون میگیریم:
// 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 کارت را عوض کنید و بقیهی فایل دستنخورده میماند:
// app/(public)/posts/page.tsx (فقط خط import)
// قبل:
// import PostCard from "./_components/PostCard";
// بعد:
import PostCard from "@/components/PostCard";گام ۳: کامپوننت LatestPosts
// 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 بودنش درست و کمهزینه است.
گام ۴: صفحهی اصلی
// 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 میتواند دادهی مورد نیازش را خودش بگیرد و صفحه فقط آنها را کنار هم میگذارد.
آزمایش: اثبات اجرا فقط روی سرور
- داخل
LatestPosts، اولین خط تابع،console.log("LatestPosts روی سرور اجرا شد");را اضافه کنید. - صفحهی
http://localhost:3000را ریفرش کنید. - پیام را در ترمینال ببینید و بعد کنسول مرورگر (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 تبدیل کنیم.
