مدل کامپوننتها و رندر در App Router
وارد فصل چهارم شدهایم؛ فصلی که مهمترین تغییر ذهنی App Router را توضیح میدهد. تا اینجا بارها دیدیم که «کامپوننتها بهصورت پیشفرض Server Component هستند» و فقط بعضی فایلها (مثل NavLink و SearchBox) با "use client" شروع میشدند. اکنون وقت آن است که ببینیم این تفاوت از نظر فنی یعنی چه: کد هر کامپوننت کجا اجرا میشود، چه چیزی به مرورگر میرسد و صفحه از چه مراحلی عبور میکند. این درس نقشهی کلی است و جزئیات هر بخش را در درسهای بعدی همین فصل میبینیم.
سه جا که کد میتواند اجرا شود
در یک برنامهی Next.js، کدِ شما در سه موقعیت متفاوت اجرا میشود:
| محیط | زمان | مثال |
|---|---|---|
| سرور در زمان Build | هنگام اجرای npm run build |
ساخت صفحات استاتیک، مثل صفحههای generateStaticParams |
| سرور در زمان درخواست | وقتی کاربر صفحهای را درخواست میکند | رندر صفحهی /posts که searchParams میخواند |
| مرورگر کاربر | بعد از رسیدن JavaScript به مرورگر | کلیک روی دکمهی «بازگشت» یا تایپ در فیلد جستوجو |
هیچکدام از این محیطها «بهتر» نیست؛ هرکدام برای کاری مناسبترند. مدل کامپوننتها در App Router دقیقاً به شما اجازه میدهد برای هر بخش از رابط کاربری تعیین کنید در کدام محیط اجرا شود.
دو نوع کامپوننت
| ویژگی | Server Component | Client Component |
|---|---|---|
| نحوهی تعریف | پیشفرض؛ هیچ دستور خاصی لازم نیست | فایل با "use client" شروع میشود |
| محل اجرا | فقط روی سرور | روی سرور (برای HTML اولیه) و در مرورگر |
| کد آن به مرورگر فرستاده میشود؟ | خیر | بله |
| دسترسی به پایگاه داده، فایلسیستم و رازها | بله (مستقیم و امن) | خیر |
useState، useEffect و رویدادهایی مثل onClick |
خیر | بله |
APIهای مرورگر (window، localStorage) |
خیر | بله |
میتواند async باشد؟ |
بله | خیر |
یک نکتهی مهم که اغلب اشتباه برداشت میشود: Client Component به این معنی نیست که «فقط در مرورگر اجرا میشود». برای بارگذاری اولیه، Next.js آن را هم روی سرور رندر میکند تا کاربر فوراً HTML ببیند. سپس در مرورگر دوباره به آن «جان» میدهد. تفاوت واقعی این است که کد Client Component به مرورگر هم فرستاده میشود، ولی کد Server Component نه.
صفحه از چه مراحلی عبور میکند؟
وقتی کاربر برای اولین بار یک صفحه را باز میکند، این اتفاقات رخ میدهد:
مرحلهی سرور
- React همهی Server Componentها را اجرا میکند. نتیجه یک ساختار فشرده به نام RSC Payload (React Server Component Payload) است.
- RSC Payload شامل خروجی رندرشدهی Server Componentها، جایگاه Client Componentها همراه با مرجع فایل JavaScriptشان و Propsهایی است که از Server به Client داده شدهاند.
- Next.js از RSC Payload و دستورهای Client Componentها یک سند HTML میسازد.
مرحلهی مرورگر
- مرورگر HTML را میگیرد و فوراً نمایش میدهد. این صفحه هنوز تعاملی نیست.
- RSC Payload برای هماهنگکردن درخت کامپوننتها در سمت کلاینت استفاده میشود.
- JavaScript مربوط به Client Componentها اجرا میشود و Hydration انجام میشود: React رویدادها را به HTML موجود وصل میکند و صفحه تعاملی میشود.
میتوان آن را با یک تصویر ساده فهمید: HTML مثل یک کالبد آماده است که فوراً دیده میشود، و Hydration مثل وصلکردن سیمکشی به آن است تا دکمهها کار کنند.
در ناوبریهای بعدی
بعد از بارگذاری اول، وقتی با Link به صفحهی دیگری میروید، مرورگر بهجای یک سند HTML کامل، فقط RSC Payload بخش تغییرکرده را از سرور میگیرد و React همان را بهروز میکند. Layoutهای مشترک دوباره ساخته نمیشوند. این همان رفتاری است که در درس «Prefetching و رفتار Navigation» دیدیم.
مرز Server و Client: دستور "use client"
دستور "use client" در ابتدای فایل مرز بین دو دنیا را تعیین میکند:
- فایلی که
"use client"دارد و همهی ماژولهایی که از آن import میکند، در بستهی JavaScript مرورگر قرار میگیرند. - فایلهایی که
"use client"ندارند و از Client Component import نشدهاند، Server Component هستند. - لازم نیست دستور را در همهی فایلهای یک زیردرخت بنویسید؛ همینکه یک فایل مرز را تعیین کرد، همهی ماژولهای importشدهی آن Client به حساب میآیند.
یک Server Component میتواند Client Component را در درخت خودش رندر کند، و برعکس، یک Client Component میتواند Server Component را بهصورت children بگیرد. قاعدهها و الگوهای ترکیب این دو را در درس «ترکیب Server و Client Components» میبینیم.
درخت کامپوننتهای DevNotes
ببینید پروژهی ما تا اینجا چگونه بین این دو دنیا تقسیم شده است. صفحهی /posts را در نظر بگیرید:
RootLayout ← Server
└── PublicLayout ← Server
├── SiteHeader ← Server
│ └── NavLink (×4) ← Client ("use client": usePathname)
├── PostsLayout ← Server
│ └── PostsPage ← Server (async، searchParams، getPosts)
│ ├── PostsIntro ← Server
│ ├── SearchBox ← Client ("use client": useState، useRouter)
│ ├── PostCard (×3) ← Server
│ └── Pagination ← Server
└── SiteFooter ← Server
اکثر کامپوننتهای ما Server Component هستند و فقط دو کامپوننت کوچک به مرورگر میروند: NavLink (چون usePathname لازم دارد) و SearchBox (چون State و رویداد دارد). هرچه تعداد و حجم Client Componentها کمتر باشد، JavaScript کمتری به مرورگر فرستاده میشود و صفحه سریعتر تعاملی میشود. این اصل را در درس «انتخاب محل اجرای کد و کاهش JavaScript سمت کلاینت» ادامه میدهیم.
عملی: دیدن مرز Server و Client با console.log
برای اینکه این مدل را با چشم خودتان ببینید، یک آزمایش کوتاه انجام میدهیم. این تغییرها موقتی هستند و در پایان حذف میشوند.
گام ۱: لاگهای موقت
در components/SiteHeader.tsx یک خط به ابتدای تابع اضافه کنید:
// components/SiteHeader.tsx (تغییر موقت)
import NavLink from "@/components/NavLink";
export default function SiteHeader() {
console.log("SiteHeader رندر شد (Server Component)");
return (
<header>
<strong>DevNotes</strong>
<nav aria-label="ناوبری اصلی">
<ul>
<li><NavLink href="/">خانه</NavLink></li>
<li><NavLink href="/posts">نوشتهها</NavLink></li>
<li><NavLink href="/about">درباره</NavLink></li>
<li><NavLink href="/login">ورود</NavLink></li>
</ul>
</nav>
</header>
);
}
در components/NavLink.tsx هم یک خط به ابتدای تابع NavLink اضافه کنید (بقیهی فایل را دست نزنید):
// components/NavLink.tsx (تغییر موقت، فقط داخل تابع NavLink)
export default function NavLink({ href, children }: NavLinkProps) {
console.log("NavLink رندر شد:", href);
const pathname = usePathname();
// ... بقیهی کد بدون تغییر
}گام ۲: مشاهده
- سرور توسعه را اجرا کنید و صفحهی
http://localhost:3000را یکبار ریفرش کنید. - ترمینال (جایی که
npm run devاجرا شده) را ببینید. - کنسول ابزار توسعهدهندهی مرورگر (تب Console) را باز کنید و دوباره صفحه را ریفرش کنید.
- با کلیک روی لینکها بین
/،/postsو/aboutجابهجا شوید و ترمینال و کنسول را نگاه کنید.
نتیجهی مورد انتظار
| لاگ | ترمینال (سرور) | کنسول مرورگر |
|---|---|---|
SiteHeader رندر شد |
در بارگذاری اول ظاهر میشود | در بارگذاری اول از سرور بازپخش میشود و ممکن است با برچسب Server دیده شود؛ اما کد این کامپوننت هرگز در مرورگر اجرا نمیشود |
NavLink رندر شد |
در بارگذاری اول (۴ بار، یک بار برای هر لینک) چون Client Component برای HTML اولیه روی سرور هم رندر میشود | در Hydration و هر بار که مسیر عوض میشود (برای هر ۴ لینک) |
دو چیز را باید دقیق ببینید:
NavLinkهم در ترمینال و هم در مرورگر لاگ میدهد؛ این همان نکتهی «Client Component هم روی سرور رندر میشود» است. در حالت توسعه ممکن است هر لاگ مرورگر دوبار دیده شود، چون React در Strict Mode برای کشف خطا کامپوننتها را عمداً دوبار رندر میکند.- هنگام جابهجایی بین صفحات، لاگ
SiteHeaderدوباره در ترمینال ظاهر نمیشود، چون Layout دوباره رندر نمیشود، ولیNavLinkدر مرورگر دوباره اجرا میشود چونusePathnameتغییر کرده است. این دقیقاً همان «حفظ Layoutها» در ناوبری است.
گام ۳: دیدن RSC Payload در سورس صفحه
در مرورگر روی صفحه کلیک راست و «View Page Source» را بزنید. در سورس باید هم HTML کامل هدر و لینکها را ببینید (که نشان میدهد رندر روی سرور انجام شده) و هم در انتها چند تگ <script> که معمولاً شامل عبارت self.__next_f.push هستند. این اسکریپتها همان RSC Payload هستند که Next.js داخل HTML گذاشته است. نیازی نیست محتوای آنها را بفهمید؛ فقط بدانید چنین چیزی وجود دارد.
گام ۴: پاککردن لاگها
دو خط console.log را از SiteHeader و NavLink حذف کنید تا پروژه به حالت قبل برگردد.
چرا این مدل مفید است؟
- JavaScript کمتر برای مرورگر: کد Server Componentها و کتابخانههایی که فقط آنها استفاده میکنند، به مرورگر نمیرسد.
- دسترسی مستقیم به داده: Server Component میتواند از پایگاه داده یا فایلسیستم بخواند، بدون اینکه برای هر کار یک API جداگانه بسازیم.
- امنیت بهتر: کلیدها و رازها در سرور میمانند. البته باید مراقب باشیم کد حساس به Client Component نرسد؛ این را در درس «server-only، Context و اشتباهات رایج» میبینیم.
- HTML اولیهی کامل: محتوا از اولین پاسخ آماده است، که برای سرعت درکشده و موتورهای جستوجو مفید است.
- تعامل فقط جایی که لازم است: فقط بخشهایی که واقعاً تعاملیاند Hydrate میشوند.
نکتههای مهم
- نام «Server Component» به معنی «SSR» نیست. Server Component یعنی کدش فقط در سرور اجرا میشود؛ SSR یعنی صفحهای که روی سرور و هنگام درخواست به HTML تبدیل میشود. Client Component هم میتواند Pre-render شود. رابطهی دقیق این مفاهیم با رندر استاتیک و پویا را در درس «مدلهای رندر: Static، Dynamic، SSR، SSG و ISR» از فصل «Rendering و Data Fetching» میبینیم.
- Server Component بودن به معنی کششدن یا استاتیکبودن نیست. یک Server Component هنگام Build یا هنگام درخواست اجرا میشود و این به نوع رندر و کش بستگی دارد، نه به خود نوع کامپوننت. موضوع فصل «Cache و Revalidation در Next.js 16» است.
-
Hydration نیاز دارد HTML سرور و کلاینت یکسان باشد. اگر یک Client Component در سرور و مرورگر خروجی متفاوت تولید کند (مثلاً بهخاطر
Date.now()یاMath.random()هنگام رندر)، React هشدار «Hydration mismatch» میدهد. - پیشفرض را عوض نکنید مگر لازم باشد. قاعدهی کلی این است که همهچیز Server Component بماند و فقط کوچکترین بخش تعاملی را Client کنید، مثل همان دو کامپوننتی که تا اینجا داریم.
جمعبندی
- کد در Next.js میتواند در سه محیط اجرا شود: سرور در زمان Build، سرور در زمان درخواست و مرورگر.
- Server Component پیشفرض است، فقط روی سرور اجرا میشود و کدش به مرورگر نمیرسد؛ Client Component با
"use client"تعریف میشود و هم برای HTML اولیه روی سرور رندر میشود و هم در مرورگر Hydrate میشود. - صفحه با RSC Payload و HTML از سرور میآید؛ مرورگر HTML را نمایش میدهد و با Hydration، Client Componentها را تعاملی میکند.
- در ناوبریهای بعدی فقط RSC Payload بخش تغییرکرده دریافت میشود و Layoutهای مشترک حفظ میشوند.
- در DevNotes اکثر کامپوننتها Server هستند و فقط
NavLinkوSearchBox(وBackButton) Clientاند. - با
console.logمیتوان تفاوت محل اجرا را مشاهده کرد. - در درس بعد، «Server Components و کاربرد آنها»، دقیقتر میبینیم Server Component چه کارهایی میتواند انجام دهد و چه محدودیتهایی دارد.
