قسمت نهم - Server Components و Client Components در Next.js
در Next.js 16، صفحهها و layoutها بهصورت پیشفرض Server Component هستند.فقط وقتی به قابلیتهای مرورگر یا تعامل کاربر نیاز داریم، باید سراغ Client Component برویم.
Server Components در Next.js: چرا بیشتر صفحات باید سروری باشند؟
در App Router، یک نکته خیلی مهم وجود دارد که باید از همان اول درست درکش کنیم:
در Next.js 16، صفحهها و layoutها بهصورت پیشفرض Server Component هستند.
یعنی وقتی در app/ یک page.tsx یا layout.tsx میسازی، لازم نیست کاری انجام بدهیم تا سروری شود. رفتار پیشفرض همین است.
فقط وقتی به قابلیتهای مرورگر یا تعامل کاربر نیاز داریم، باید سراغ Client Component برویم.
این موضوع از نظر معماری پروژه خیلی مهم است، مخصوصاً در پروژهای مثل پرتفولیو، بلاگ، CMS و داشبورد که بخش زیادی از UI در اصل محتوایی است، نه تعاملی.
Server Component چیست؟
Server Component کامپوننتی است که روی سرور رندر میشود.
در Next.js، این کامپوننتها میتوانند:
- نزدیک به منبع داده، داده را دریافت کنند
- بدون ارسال منطق اضافی به مرورگر، UI را تولید کنند
- نتیجه را به کلاینت بفرستند
- در صورت نیاز، خروجی را cache یا stream کنند
طبق داک رسمی، در سمت سرور این فرایند با استفاده از React انجام میشود و خروجی Server Componentها به فرمت مخصوصی به نام RSC Payload تبدیل میشود.
سپس Next.js از همین payload بههمراه Client Componentها برای prerender کردن HTML استفاده میکند.
در اولین لود صفحه، مرورگر این روند را تجربه میکند:
- HTML خیلی سریع نمایش داده میشود
RSC Payloadبرای هماهنگ کردن tree سرور و کلاینت استفاده میشود- جاوااسکریپت فقط برای hydrate کردن Client Componentها اجرا میشود
پس Server Component یعنی:
بخشهایی از UI که لازم نیست روی مرورگر اجرا شوند، روی سرور رندر شوند.
مزایا
استفاده از Server Componentها چند مزیت مستقیم دارد که داک رسمی هم روی آنها تأکید میکند:
- دریافت داده از دیتابیس یا API نزدیک به منبع
- استفاده از secretها مثل token و API key بدون افشای آنها در مرورگر
- کاهش جاوااسکریپتی که به مرورگر ارسال میشود
- بهبود
First Contentful Paint (FCP) - امکان stream کردن تدریجی محتوا
در عمل یعنی اگر صفحهای بیشتر کارش نمایش داده است، بهتر است سروری بماند.
مثلاً در یک صفحه بلاگ یا صفحه جزئیات پروژه، معمولاً لازم نیست کل صفحه client-side شود.
کاربر فقط میخواهد محتوا را سریع ببیند. پس اینکه این صفحه روی سرور رندر شود، تصمیم درستتری است.
کاربرد در پروژه محتوایی
در پروژهای مثل پرتفولیو شخصی که شامل این بخشهاست:
- صفحه اصلی
- About
- Contact
- لیست پروژهها
- لیست بلاگ
- صفحه جزئیات پروژه
- صفحه جزئیات پست
بخش بزرگی از این صفحات محتوامحور هستند.
یعنی:
- داده میگیرند
- UI میسازند
- لینک نمایش میدهند
- معمولاً state محلی پیچیده ندارند
- وابسته به
windowیاlocalStorageنیستند
پس این صفحات بهصورت طبیعی باید Server Component باشند.
برای مثال:
app/page.tsxapp/about/page.tsxapp/contact/page.tsxapp/projects/page.tsxapp/projects/[slug]/page.tsxapp/blog/page.tsxapp/blog/[slug]/page.tsx
همه اینها در حالت عادی بهتر است سروری بمانند.
محدودیتها
Server Componentها با وجود مزایای زیاد، محدودیتهایی هم دارند.
طبق داک رسمی، وقتی به این موارد نیاز داری، دیگر Server Component بهتنهایی کافی نیست:
state- event handlerهایی مثل
onClickوonChange - lifecycle logic مثل
useEffect - browser APIها مثل
window,localStorage,Navigator.geolocation - custom hookهایی که به قابلیتهای کلاینت وابستهاند
- React context در خود Server Component
به زبان ساده، اگر کامپوننت تو نیاز دارد به تعامل زنده کاربر واکنش نشان بدهد، آن قسمت باید Client Component باشد.
مثال در پروژه ما
بیاییم این موضوع را در همان پروژه پرتفولیو خودمان ببینیم.
فرض کن صفحه لیست پروژهها را اینطور داریم:
// app/projects/page.tsx
import { ProjectsIntroSection } from '@/components/sections/projects/projects-intro-section'
import { ProjectsGridSection } from '@/components/sections/projects/projects-grid-section'
export default function ProjectsPage() {
return (
<div className="space-y-16">
<ProjectsIntroSection />
<ProjectsGridSection />
</div>
)
}
این صفحه هیچ نیازی به use client ندارد.
چرا؟
- فقط UI را میچیند
- از چند section تشکیل شده
- state ندارد
- event handler ندارد
- به APIهای مرورگر وابسته نیست
اگر بعداً ProjectsGridSection را به Prisma وصل کنیم، باز هم همچنان سروری ماندنش منطقی است:
// components/sections/projects/projects-grid-section.tsx
import { prisma } from '@/lib/prisma'
import { ProjectCard } from '@/components/projects/project-card'
export async function ProjectsGridSection() {
const projects = await prisma.project.findMany({
orderBy: {
createdAt: 'desc',
},
})
return (
<section>
<div className="mt-8 grid gap-6 md:grid-cols-2">
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
</section>
)
}
این دقیقاً یکی از مهمترین کاربردهای Server Component است:
داده را از دیتابیس روی سرور بگیر، بعد UI را همانجا بساز.
چه زمانی Client Component لازم است؟
طبق داک رسمی، Client Component زمانی لازم است که به تعامل یا قابلیتهای مرورگر نیاز داشته باشی.
در پروژه ما چند مثال روشن داریم:
- منوی موبایل در
Header - فرم تماس
- دکمه لایک یا bookmark
- modal
- tab switcher
- theme toggle
- file upload UI
- preview تصویر قبل از آپلود
برای مثال، منوی موبایل هدر چون useState و onClick دارد، باید Client Component باشد:
'use client'
import { useState } from 'react'
export function HeaderMenu() {
const [open, setOpen] = useState(false)
return (
<button onClick={() => setOpen((value) => !value)}>
Toggle Menu
</button>
)
}
اما خود layout اصلی سایت لازم نیست client-side شود.
این نکته خیلی مهم است.
داک رسمی صریحاً میگوید برای کاهش حجم bundle بهتر است فقط بخش تعاملی کوچک را Client Component کنیم، نه کل layout یا کل صفحه را.
پس این تصمیم درستتر است:
app/layout.tsxسروری بماند- فقط
Headerیا فقط بخش منوی تعاملی داخل آن client باشد
نه اینکه کل layout را با use client به کامپوننت کلاینتی تبدیل کنیم.
نکته مهم درباره use client
'use client' فقط یک برچسب ساده نیست.
این directive یک boundary بین module graph سرور و کلاینت ایجاد میکند.
طبق داک رسمی، وقتی یک فایل با 'use client' علامتگذاری شود:
- خود آن فایل client میشود
- importهای آن فایل هم وارد client bundle میشوند
- کامپوننتهایی که مستقیم render میکند هم در graph کلاینت قرار میگیرند
یعنی نباید بیدلیل آن را روی فایلهای بزرگ یا ریشهای بگذاری.
مثلاً اگر کل layout.tsx را client کنی، بخش زیادی از UI استاتیک هم بیدلیل وارد bundle مرورگر میشود.
این دقیقاً چیزی است که باید از آن دوری کنیم.
Server و Client کنار هم
یکی از الگوهای مهمی که داک رسمی توضیح میدهد این است که Server Component و Client Component قرار نیست از هم جدا و بیارتباط باشند.
میتوانی آنها را با هم compose کنی.
مثلاً:
- صفحه پروژه سروری باشد
- داخل آن یک دکمه تعاملی یا modal کلاینتی قرار بگیرد
نمونه ساده:
// app/projects/[slug]/page.tsx
import ProjectActions from '@/components/projects/project-actions'
import { getProject } from '@/lib/data'
export default async function ProjectPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const project = await getProject(slug)
return (
<section>
<h1>{project.title}</h1>
<p>{project.description}</p>
<ProjectActions projectSlug={project.slug} />
</section>
)
}
// components/projects/project-actions.tsx
'use client'
import { useState } from 'react'
type ProjectActionsProps = {
projectSlug: string
}
export default function ProjectActions({
projectSlug,
}: ProjectActionsProps) {
const [saved, setSaved] = useState(false)
return (
<button onClick={() => setSaved((value) => !value)}>
{saved ? 'Saved' : `Save ${projectSlug}`}
</button>
)
}
اینجا صفحه اصلی سروری است، ولی بخش تعاملی آن کلاینتی است.
این همان مدلی است که در App Router باید پیشفرض ذهنی تو باشد.
جمعبندی
در Next.js 16، بیشتر صفحهها و layoutها باید بهصورت پیشفرض Server Component باقی بمانند.
چون این مدل:
- برای دریافت داده از دیتابیس عالی است
- secretها را در سرور نگه میدارد
- جاوااسکریپت ارسالی به مرورگر را کمتر میکند
- برای پروژههای محتوایی مثل پرتفولیو و بلاگ انتخاب طبیعیتری است
- امکان رندر سریعتر و stream شدن محتوا را فراهم میکند
در پروژه ما، بیشتر routeهای محتوایی مثل صفحه اصلی، About، Contact، Projects و Blog باید سروری باشند.
فقط بخشهایی که واقعاً تعامل میخواهند، باید به Client Component تبدیل شوند.
Client Components در Next.js: چه زمانی به use client نیاز داریم؟
بعد از اینکه فهمیدیم در App Router بیشتر صفحهها بهتر است سروری باشند، حالا باید دقیق بفهمیم چه زمانی واقعاً به Client Component نیاز داریم.
این بخش مهم است، چون یکی از رایجترین اشتباهها در پروژههای Next.js این است که توسعهدهنده خیلی زود و خیلی گسترده از 'use client' استفاده میکند.
نتیجه این کار معمولاً این است:
- bundle مرورگر بزرگتر میشود
- مزیت Server Componentها کم میشود
- ساختار پروژه بیدلیل client-heavy میشود
پس باید مرز را دقیق بشناسیم.
Client Component چیست؟
Client Component کامپوننتی است که روی کلاینت اجرا میشود و برای فعال شدن باید در بالای فایلش از directive زیر استفاده شود:
'use client'
این directive باید بالای importها قرار بگیرد.
مثلاً:
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
)
}
طبق داک رسمی، با افزودن 'use client' تو یک boundary بین server و client تعریف میکنی.
تفاوت با Server Component
تفاوت اصلی از نظر کاربرد این است:
Server Component
- برای دریافت داده
- برای رندر محتوای استاتیک یا محتوایی
- برای کاهش JavaScript سمت مرورگر
- برای استفاده از secretها و منطق سروری
Client Component
- برای state
- برای event handling
- برای
useEffect - برای browser API
- برای custom hookهای وابسته به کلاینت
یعنی سؤال اصلی این نیست که «این کامپوننت کوچک است یا بزرگ؟»
سؤال اصلی این است که:
آیا این کامپوننت به قابلیتهای مرورگر یا تعامل زنده نیاز دارد یا نه؟
state و event handling
مهمترین نشانه اینکه به Client Component نیاز داری، وجود state و event handler است.
اگر کامپوننت تو از این موارد استفاده میکند:
useStateuseReduceronClickonChangeonSubmit
باید client باشد.
مثال ساده:
'use client'
import { useState } from 'react'
export function ContactFormPreview() {
const [message, setMessage] = useState('')
return (
<form>
<textarea
value={message}
onChange={(event) => setMessage(event.target.value)}
/>
<p>{message.length} characters</p>
</form>
)
}
چون اینجا:
- state داریم
onChangeداریم- واکنش زنده به ورودی کاربر داریم
پس کامپوننت باید client باشد.
browser API
وقتی به APIهای مخصوص مرورگر نیاز داری، باز هم باید Client Component بسازی.
طبق داک رسمی، نمونههای رایج اینها هستند:
windowlocalStorageNavigator.geolocation
مثال:
'use client'
import { useEffect, useState } from 'react'
export function ThemePreference() {
const [theme, setTheme] = useState('dark')
useEffect(() => {
const savedTheme = window.localStorage.getItem('theme')
if (savedTheme) {
setTheme(savedTheme)
}
}, [])
return <p>{theme}</p>
}
این کد در Server Component قابل اجرا نیست، چون window فقط در مرورگر وجود دارد.
مثال در منو و فرم
در پروژه خودمان، دو مثال خیلی روشن برای Client Component داریم.
۱. منوی موبایل در Header
هدر سایت شاید بیشتر ظاهرش استاتیک باشد، اما منوی موبایل:
- باز و بسته میشود
useStateدارد- روی click واکنش نشان میدهد
پس همان بخش باید client باشد.
'use client'
import { useState } from 'react'
import { Menu, X } from 'lucide-react'
export function Header() {
const [isOpen, setIsOpen] = useState(false)
return (
<header>
<button
type="button"
onClick={() => setIsOpen((value) => !value)}
aria-expanded={isOpen}
aria-label="Toggle navigation menu"
>
{isOpen ? <X /> : <Menu />}
</button>
</header>
)
}
۲. فرم تماس
فرم تماس هم معمولاً حداقل یکی از اینها را دارد:
onSubmit- کنترل inputها
- error state
- loading state
- validation feedback
پس اگر فرم بهصورت تعاملی پیاده شود، آن بخش client خواهد بود.
هزینه استفاده زیاد
داک رسمی روی یک نکته خیلی مهم تأکید میکند:
برای کاهش اندازه client JavaScript bundle، باید 'use client' را فقط روی بخشهای تعاملی بگذاری.
این یعنی اگر بیدلیل فایلهای بزرگ را client کنی، این هزینه را میدهی:
- JavaScript بیشتری به مرورگر فرستاده میشود
- مزیت معماری سروری کمتر میشود
- performance اولیه ضعیفتر میشود
- نگهداری مرز server/client سختتر میشود
پس این الگو بهتر است:
layout.tsxسروریpage.tsxسروری- sectionهای محتوایی سروری
- فقط
mobile-menu.tsxیاcontact-form.tsxکلاینتی
نه اینکه همه چیز را با یک تصمیم کلی client-side کنیم.
داده از Server به Client
یکی از بخشهای مهم داک رسمی این است که Server Component میتواند داده را با props به Client Component بدهد.
مثلاً:
// app/blog/[slug]/page.tsx
import LikeButton from '@/components/blog/like-button'
import { getPost } from '@/lib/data'
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return (
<article>
<h1>{post.title}</h1>
<LikeButton likes={post.likes} />
</article>
)
}
// components/blog/like-button.tsx
'use client'
import { useState } from 'react'
type LikeButtonProps = {
likes: number
}
export default function LikeButton({ likes }: LikeButtonProps) {
const [count, setCount] = useState(likes)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
نکته مهم داک رسمی این است که propهایی که به Client Component میدهی باید serializable باشند.
یک نکته مهم درباره context
طبق داک رسمی، React context در Server Component پشتیبانی نمیشود.
اگر بخواهی provider بسازی، باید provider را در یک Client Component قرار بدهی و بعد آن را داخل یک Server Component مثل layout.tsx render کنی.
مثلاً اگر بعداً برای theme یا auth UI context داشته باشی، provider باید client باشد، نه خود کل layout.
همچنین داک رسمی توصیه میکند providerها را تا حد ممکن عمیقتر در tree قرار بدهی، نه اینکه بیدلیل کل سند را با آنها بپوشانی.
این کار به Next.js کمک میکند بخشهای استاتیک را بهتر optimize کند.
جمعبندی
در Next.js 16، به use client فقط زمانی نیاز داری که کامپوننت تو واقعاً به این موارد وابسته باشد:
- state
- event handler
useEffect- browser API
- custom hook
- context provider
- تعامل مستقیم کاربر
در بقیه مواقع، بهتر است کامپوننت سروری بماند.
برای پروژه ما این یعنی:
- صفحهها و sectionهای محتوایی معمولاً Server Component باشند
- منوی موبایل، فرمها، modalها، uploaderها و کنترلهای تعاملی Client Component باشند
'use client'فقط روی کوچکترین مرز لازم قرار بگیرد
این نگاه باعث میشود پروژه هم از نظر performance بهتر باشد، هم از نظر معماری تمیزتر.
قدم بعدی منطقی بعد از این آموزش میتواند یکی از اینها باشد:
Dynamic Routeبرای صفحه جزئیات پروژه- ساخت فرم تماس با
Server ActionsوZod - ساخت صفحه لیست بلاگ و جزئیات پستها
- اتصال پروژهها به Prisma