تا اینجا با مفاهیم پایهای کامپوننتهای سرور و کلاینت آشنا شدیم. حالا زمان آن است که الگوهای پیادهسازی، نحوه ترکیب (Interleaving) آنها، استفاده از Context و روشهای جلوگیری از نشت اطلاعات حساس به کلاینت را بررسی کنیم.
دایرکتیو "use client" دقیقاً چه مرزی در درخت ماژولها ایجاد میکند؟
ترفند کاهش حجم جاوااسکریپت کلاینت (Bundle Size) با کوچک کردن مرزها
نحوه ترکیب کامپوننتهای سرور درون کلاینت به کمک ویژگی children
استفاده از Context Providerها و المانهای شخص ثالث (Third-party)
جلوگیری از نشت کدهای محرمانه سرور با پکیج server-only
"use client" به عنوان یک مرزوقتی عبارت "use client" را در بالاترین خط یک فایل (قبل از تمام importها) مینویسید، در حال تعریف یک مرز (Boundary) بین درخت ماژولهای سرور و کلاینت هستید.
یک قانون بسیار مهم: نیازی نیست لزوماً بالای تکتک فایلهای کلاینتی این دایرکتیو را بنویسید. به محض اینکه یک فایل با "use client" نشانهگذاری شود، تمام فایلهایی که درون آن import میشوند و تمام کامپوننتهای فرزندی که مستقیماً درون آن رندر میگردند، به طور خودکار عضو باندل کلاینت محسوب میشوند.
برای اینکه سرعت لود سایت بالا بماند، نباید بخشهای بزرگی از صفحه را بیدلیل کلاینتی کنید. به جای کلاینتی کردن کل یک کامپوننت بزرگ (مثل کل Layout یا هدر)، فقط بخش تعاملی کوچک را جدا کرده و "use client" بزنید.
به این مثال نگاه کنید؛ ما یک کامپوننت ساختار اصلی (Layout) داریم که کاملاً استاتیک است (مثل لوگو و منو)، اما یک نوار سرچ تعاملی درون خود دارد:
// app/layout.tsx
import Search from './search' // کلاینت کامپوننت
import Logo from './logo' // سرور کامپوننت
// این کامپوننت به طور پیشفرض سروری میماند و حجم باندل را کم میکند
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<Search /> {/* فقط این بخش کوچک، جاوااسکریپت به مرورگر میفرستد */}
</nav>
<main>{children}</main>
</>
)
}
// app/ui/search.tsx
'use client'
export default function Search() {
// کدهای تعاملی سرچ و افکتها...
}
شما میتوانید دادههای دریافتی از دیتابیس را در سرور کامپوننت بگیرید و مستقیم از طریق props به کلاینت کامپوننت بفرستید:
// app/[id]/page.tsx (Server Component)
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await getPost(id) // دریافت دیتا روی سرور
return <LikeButton likes={post.likes} /> // ارسال به کلاینت
}
⚠️ یک شرط حیاتی: تمام پروپهایی که از سرور کامپوننت به کلاینت کامپوننت پاس داده میشوند، باید قابل سریالسازی (Serializable) باشند. یعنی کلاینت بتواند آنها را به رشته (String) تبدیل کند. انواع دادههای بومی مثل اشیاء ساده (Plain Objects)، آرایهها، رشتهها، اعداد و بانیولینها مجاز هستند؛ اما شما نمیتوانید یک «تابع» (Function) را از سرور به کلاینت پاس دهید.
یک سوال مکرر: «آیا میتوان یک سرور کامپوننت را درون یک کلاینت کامپوننت رندر کرد؟» پاسخ این است: مستقیماً با import خیر، اما از طریق پروپِ children بله!
اگر یک سرور کامپوننت را مستقیماً درون یک فایل کلاینتی import کنید، آن کامپوننت به ناچار تبدیل به کلاینت کامپوننت میشود. برای حل این مشکل، ساختار کلاینت را طوری طراحی کنید که یک Slot یا جایگاه به نام children بپذیرد:
// app/ui/modal.tsx
'use client'
export default function Modal({ children }: { children: React.ReactNode }) {
return <div className="modal-box">{children}</div>
}
// app/page.tsx (Server Component)
import Modal from './ui/modal' // کلاینت
import Cart from './ui/cart' // سرور (دیتای سبد خرید را مستقیم از سرور میگیرد)
export default function Page() {
return (
<Modal>
{/* کامپوننت سبد خرید روی سرور رندر شده و خروجی آمادهاش به مودال تزریق میشود */}
<Cart />
</Modal>
)
}
در این الگو، کامپوننت <Cart> پیش از هر چیز روی سرور رندر میشود و خروجی متنی آن به پِیلودِ RSC Payload اضافه شده و در جایگاه تعاملی مودال قرار میگیرد.
ابزار React Context برای اشتراکگذاری دادههای سراسری (مثل تم تاریک/روشن) فوقالعاده است، اما کامپوننتهای سروری به دلیل ماهیت بیوضعیت (Stateless) خود از Context پشتیبانی نمیکنند.
برای حل این موضوع، باید پرووایدر را درون یک کلاینت کامپوننت بسازید و فرزندان را پذیرا باشید:
// app/theme-provider.tsx
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
حالا این پرووایدر کلاینتی را در بالاترین سطح فایل سروری layout.tsx ایمپورت کرده و {children} را با آن بپوشانید:
// app/layout.tsx (Server Component)
import ThemeProvider from './theme-provider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{/* پرووایدر کلاینتی، صفحات سروری زیرمجموعه را مدیریت میکند */}
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
💡 نکته حرفهای: پرووایدرها را تا حد امکان در گرههای پایینتر درخت رندر قرار دهید. توجه کنید که در کد بالا ما کل تگ
<html>را درون پرووایدر نپیچیدیم، بلکه فقط{children}را پوشش دادیم تا Next.js بتواند بخشهای استاتیک تگ HTML را بهینهسازی کند.
بسیاری از کتابخانههای قدیمی NPM (مثل کامپوننتهای اسلایدر یا کاروسل) با اینکه از کدهای اختصاصی مرورگر مثل useState استفاده میکنند، اما هنوز عبارت "use client" را بالای کدهای خود ندارند. اگر آنها را مستقیم در یک سرور کامپوننت ایمپورت کنید، با خطای کرش مواجه میشوید.
کافی است یک فایل کوچک در پروژه خود بسازید، آن را کلاینتی کرده و کامپوننت شخص ثالث را از درون آن به بیرون بفرستید (Re-export):
// app/carousel.tsx
'use client'
import { Carousel } from 'acme-carousel' // پکیج فرضی بدون use client
export default Carousel
حالا با خیال راحت میتوانید کامپوننت مهار شدهی Carousel را مستقیماً در صفحات سروری پروژه خود استفاده کنید.
server-onlyگاهی اوقات شما توابعی دارید که مستقیماً با دیتابیس صحبت میکنند یا از کدهای محرمانه مثل process.env.API_KEY استفاده میکنند. این کدها هرگز و تحت هیچ شرایطی نباید سر از مرورگر کلاینت درآورند:
// lib/data.ts
export async function getData() {
const res = await fetch('https://api.internal.com/data', {
headers: {
authorization: process.env.API_KEY, // کلید امنیتی حساس
},
})
return res.json()
}
در Next.js برای امنیت بیشتر، متغیرهای محیطی که پیشوند NEXT_PUBLIC_ ندارند در کلاینت تبدیل به یک رشته خالی ("") میشوند. بنابراین اگر اشتباهاً این تابع را در یک کلاینت کامپوننت ایمپورت کنید، کد شما خراب شده و ارور میدهد، اما بدتر از آن این است که کدهای طولانی این تابع حجم باندل کلاینت را سنگین میکنند.
server-onlyبرای اینکه مطمئن شوید هیچ برنامهنویسی در تیم شما اشتباهاً این فایل را در بخش کلاینت لود نمیکند، پکیج رسمی server-only را نصب کنید:
pnpm add server-only
سپس آن را در خط اول فایلهای دیتای سروری خود ایمپورت کنید:
// lib/data.ts
import 'server-only' // ضامن عدم ورود این فایل به کلاینت
export async function getData() {
// ... کدهای حساس سرور
}
حالا اگر کسی تلاش کند متد getData را در فایلی که مارک کلاینتی دارد ایمپورت کند، پروژه در همان زمان بیلد (Build-time) با یک ارور واضح متوقف میشود و جلوی فاجعه را میگیرد.
مرزها را کوچک نگهدارید: اجازه ندهید کدهای تعاملی کوچک، تمام کامپوننتهای والد سروری را به درون باندل جاوااسکریپت کلاینت بکشند.
ارسال ایمن دیتا: فقط دادههای ساده و سریالپذیر را از طریق پروپها از سرور به کلاینت بفرستید.
تزریق به عنوان فرزند: برای داشتن کامپوننت سروری در دل کلاینت، از الگوی <Client><Server /></Client> یا همان پاس دادن به عنوان children استفاده کنید.
قرنطینه امنیتی: فایلهای مرتبط با دیتابیس و توکنها را به کمک import 'server-only' از دسترسی کلاینت دور نگه دارید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://nextjs.org/docs/app