الگوهای ترکیب سرور و کلاینت
هنگام ساخت برنامه های React، باید در نظر بگیرید که کدام قسمت های برنامه شما باید در سرور یا کلاینت رندر شوند. این صفحه برخی از الگوهای ترکیب توصیه شده هنگام استفاده از کامپوننت های سرور و کلاینت را پوشش می دهد.
چه زمانی از کامپوننت های سرور و کلاینت استفاده کنیم؟
در اینجا خلاصه ای از موارد استفاده مختلف برای کامپوننت های سرور و کلاینت آورده شده است:
| چه کار باید انجام بدهید؟ | کامپوننت سرور | کامپوننت کلاینت |
|---|---|---|
| دریافت داده | بله | خیر |
| دسترسی به منابع بک اند (مستقیم) | بله | خیر |
| اطلاعات حساس را روی سرور نگه دارید (توکن های دسترسی، کلیدهای API و غیره) | بله | خیر |
| حفظ وابستگی های بزرگ به سرور / کاهش جاوا اسکریپت سمت سرویس گیرنده | بله | خیر |
اضافه کردن تعامل و ایونت لیسنرها (onClick(), onChange() و غیره) | خیر | بله |
استفاده از استیت و افکت های لایف سایکل (useState(), useReducer(), useEffect() و غیره) | خیر | بله |
| استفاده از browser-only APIs | خیر | بله |
| استفاده از هوک های سفارشی که وابستگی دارند به استیت، افکت یا browser-only APIs | خیر | بله |
| استفاده از کلاس کامپوننت های React (opens in a new tab) | خیر | بله |
الگوهای کامپوننت سرور
قبل از انتخاب رندرینگ سمت کاربر (client-side rendering)، ممکن است بخواهید کارهایی را روی سرور مانند دریافت داده یا دسترسی به پایگاه داده یا سرویس های بک اند خود انجام دهید.
در اینجا برخی از الگوهای رایج هنگام کار با کامپوننت های سرور آورده شده است:
به اشتراک گذاری داده بین کامپوننت ها
هنگام دریافت داده در سرور، ممکن است مواردی وجود داشته باشد که نیاز به به اشتراک گذاری داده در سراسر اجزای مختلف داشته باشید. برای مثال، ممکن است یک چیدمان و یک صفحه داشته باشید که به داده های مشابه وابسته باشند.
به جای استفاده از React Context (opens in a new tab) (که در سرور در دسترس نیست) یا انتقال داده به عنوان props، می توانید از fetch یا تابع cache React برای دریافت داده های مشابه در کامپوننت هایی که به آنها نیاز دارند استفاده کنید، بدون اینکه نگران درخواست های تکراری برای داده های مشابه باشید. این به این دلیل است که React، fetch را برای به طور خودکار به خاطر سپردن memoize درخواست های داده گسترش می دهد و تابع cache زمانی که fetch در دسترس نیست قابل استفاده است.
درباره memoization در React بیشتر بیاموزید.
خارج نگه داشتن کد سرور از محیط کلاینت
از آنجایی که ماژول های جاوا اسکریپت را می توان بین هر دو ماژول کامپوننت سرور و کلاینت به اشتراک گذاشت، این امکان وجود دارد که کدی که فقط برای اجرا در سرور در نظر گرفته شده است، به طور ناخواسته وارد کلاینت شود.
به عنوان مثال، تابع دریافت داده زیر را در نظر بگیرید:
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}در نگاه اول، به نظر می رسد که getData هم در سرور و هم در کلاینت کار می کند. با این حال، این تابع حاوی یک API_KEY است که با این هدف نوشته شده است که فقط در سرور اجرا شود.
از آنجایی که متغیر محیطی API_KEY با NEXT_PUBLIC پیشوند ندارد، یک متغیر خصوصی است که فقط در سرور قابل دسترسی است. برای جلوگیری از افشای متغیرهای محیطی شما به کلاینت، Next.js متغیرهای محیطی خصوصی را با یک رشته خالی جایگزین می کند.
در نتیجه، حتی اگر getData() قابل وارد کردن و اجرا در کلاینت باشد، طبق انتظار کار نمی کند. و در حالی که عمومی کردن متغیر باعث می شود عملکرد در کلاینت کار کند، ممکن است نخواهید اطلاعات حساس را در معرض کلاینت قرار دهید.
برای جلوگیری از این نوع استفاده ناخواسته از کد سرور در کلاینت، می توانیم از پکیج server-only استفاده کنیم تا به سایر توسعه دهندگان یک خطای زمان ساخت بدهیم، اگر آنها به طور تصادفی یکی از این ماژول ها را در یک کامپوننت کلاینت وارد کنند.
برای استفاده از server-only، ابتدا پکیج را نصب کنید:
npm install server-onlyسپس پکیج را به هر ماژولی که حاوی کد سرور است وارد import کنید:
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}اکنون، هر کلاینت کامپوننت که getData() را وارد کند، یک خطای زمان ساخت دریافت می کند که توضیح می دهد این ماژول فقط در سرور قابل استفاده است.
پکیج متناظر client-only را می توان برای علامت گذاری ماژول هایی که حاوی کد فقط کلاینت هستند استفاده کرد - برای مثال، کدی که به آبجکت window دسترسی پیدا می کند.
استفاده از پکیج ها و ارائه دهندگان شخص ثالث
از آنجایی که کامپوننت های سرور یک ویژگی جدید React هستند، پکیج ها و ارائه دهندگان شخص ثالث در اکوسیستم به تازگی شروع به اضافه کردن دستورالعمل "use client" به کامپوننت هایی کرده اند که از ویژگی های فقط کلاینت مانند useState، useEffect و createContext استفاده می کنند.
امروزه، بسیاری از کامپوننت ها از پکیج های npm که از ویژگی های فقط کلاینت استفاده می کنند، هنوز این دستورالعمل را ندارند. این اجزای سازنده شخص ثالث در کامپوننت های کلاینت طبق انتظار کار می کنند زیرا دستورالعمل "use client" را دارند، اما در کامپوننت های سرور کار نمی کنند.
برای مثال، فرض کنید پکیج فرضی acme-carousel را نصب کرده اید که یک کامپوننت <Carousel /> دارد. این کامپوننت از useState استفاده می کند، اما هنوز دستورالعمل "use client" را ندارد.
اگر از <Carousel /> در یک کلاینت کامپوننت استفاده کنید، طبق انتظار کار می کند:
'use client'
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
export default function Gallery() {
let [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
)
}با این حال، اگر سعی کنید از آن به طور مستقیم در یک سرور کامپوننت استفاده کنید، با خطا مواجه خواهید شد:
import { Carousel } from 'acme-carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Error: `useState` can not be used within Server Components */}
<Carousel />
</div>
)
}این به این دلیل است که Next.js نمی داند <Carousel /> از ویژگی های فقط کلاینت استفاده می کند.
برای رفع این مشکل، می توانید کامپوننت های سازنده شخص ثالثی که به ویژگی های فقط کلاینت متکی هستند را در کلاینت کامپوننت ها خودتان بپیچید:
'use client'
import { Carousel } from 'acme-carousel'
export default Carouselاکنون می توانید از <Carousel /> به طور مستقیم در یک سرور کامپوننت استفاده کنید:
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}ما انتظار نداریم که شما نیاز به پوشش اکثر کامپوننت های شخص ثالث داشته باشید زیرا احتمالاً از آنها درون کامپوننت های کلاینت استفاده خواهید کرد. با این حال، یک استثنا پراویدرها providers هستند، زیرا آنها به استیت و context ریکت متکی هستند و به طور معمول در ریشه یک برنامه مورد نیاز هستند. در ادامه در مورد پراویدرهای context شخص ثالث بیشتر بیاموزید.
استفاده از پراویدرهای Context
پراویدرهای Context معمولاً برای به اشتراک گذاشتن دغدغههای global مانند تم جاری، در نزدیکی ریشه یک برنامه رندر میشوند. از آنجایی که React context (opens in a new tab) در سرور کامپوننت ها پشتیبانی نمی شود، تلاش برای ایجاد یک context در ریشه برنامه شما باعث ایجاد خطا خواهد شد:
import { createContext } from 'react'
// createContext is not supported in Server Components
export const ThemeContext = createContext({})
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
</body>
</html>
)
}برای رفع این مشکل، context خود را ایجاد کنید و provider آن را درون یک کلاینت کامپوننت رندر کنید:
'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>
)
}اکنون سرور کامپوننت شما قادر خواهد بود provider شما را مستقیماً رندر کند زیرا به عنوان یک کلاینت کامپوننت مشخص شده است:
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}با رندر شدن provider در ریشه، تمام کلاینت کامپوننت های دیگر در سراسر برنامه شما قادر خواهند بود این context را مصرف کنند.
خوب است بدانید: شما باید ارائه دهندگان را تا جای ممکن در درخت تعبیه کنید - توجه کنید که
ThemeProviderفقط{children}را میپوشاند به جای کل سند<html>. این باعث میشود که Next.js بخشهای استاتیک مربوط به مؤلفههای سروری شما را بهینهسازی کند.
راهنمایی برای نویسندگان کتابخانه
به طور مشابه، نویسندگان کتابخانههایی که بستههایی را برای استفاده توسط سایر توسعهدهندگان ایجاد میکنند، میتوانند از دستورالعمل "use client" برای علامتگذاری نقاط ورود client بسته خود استفاده کنند. این به کاربران بسته اجازه می دهد تا کامپوننت های بسته را مستقیماً به سرور کامپوننت های خود وارد کنند بدون اینکه نیازی به ایجاد یک مرز پوششی باشد.
شما می توانید پکیج خود را با استفاده از 'use client' عمیق تر در درخت بهینه کنید، به این ترتیب که ماژول های وارد شده بخشی از نمودار ماژول سرور کامپوننت شوند.
قابل ذکر است که برخی از باندلرها ممکن است دستورالعملهای "use client" را حذف کنند. شما می توانید نمونه ای از نحوه پیکربندی esbuild برای گنجاندن دستورالعمل "use client" در مخزن های React Wrap Balancer (opens in a new tab) و Vercel Analytics (opens in a new tab) پیدا کنید.
کلاینت کامپوننت ها
جابجایی کامپوننت های کلاینت به پایین درخت
برای کاهش اندازه باندل جاوا اسکریپت کلاینت، توصیه می کنیم کلاینت کامپوننت ها را در درخت کامپوننت خود به پایین منتقل کنید.
برای مثال، ممکن است یک طرح بندی داشته باشید که عناصر استاتیک (به عنوان مثال لوگو، لینک و غیره) و یک نوار جستجوی تعاملی دارد که از state استفاده می کند.
به جای اینکه کل طرح بندی را به یک کلاینت کامپوننت تبدیل کنید، منطق تعاملی را به یک کلاینت کامپوننت (مثلاً <SearchBar />) منتقل کنید و طرح بندی خود را به عنوان یک سرور کامپوننت نگه دارید. این بدان معناست که شما مجبور نیستید تمام کامپوننت جاوااسکریپت طرح بندی را به کلاینت ارسال کنید.
// SearchBar is a Client Component
import SearchBar from './searchbar'
// Logo is a Server Component
import Logo from './logo'
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}پاس دادن prop ها از سرور به کلاینت کامپوننت ها (Serialization)
اگر داده ها را در یک سرور کامپوننت دریافت می کنید، ممکن است بخواهید داده ها را به عنوان prop به کلاینت کامپوننت ها منتقل کنید. prop هایی که از سرور به کلاینت کامپوننت ها منتقل می شوند باید توسط React قابل سریال سازی serializable (opens in a new tab) باشند.
اگر کلاینت کامپوننت های شما به داده هایی وابسته هستند که قابل سریال سازی نیستند، می توانید داده ها را روی کلاینت با یک کتابخانه شخص ثالث یا روی سرور از طریق یک هندلر مسیر دریافت کنید fetch data on the client with a third party library.
در هم آمیختن کامپوننت های سرور و کلاینت
هنگام در هم آمیختن کامپوننت های سرور و کلاینت، ممکن است مفید باشد رابط کاربری خود را به صورت درختی از کامپوننت ها تجسم کنید. با شروع از طرح بندی ریشه که یک سرور کامپوننت است، سپس می توانید برخی از زیرمجموعه های کامپوننت ها را با افزودن دستورالعمل "use client" در سمت کاربر رندر کنید.
درون آن زیرمجموعه های کلاینت، همچنان می توانید سرور کامپوننت ها را تودرتو کنید یا اکشن های سرور را فراخوانی کنید، با این حال چند نکته وجود دارد که باید به خاطر داشته باشید:
- در طول چرخه عمر درخواست-پاسخ، کد شما از سرور به کلاینت منتقل می شود. اگر در حین کار بر روی کلاینت نیاز به دسترسی به داده یا منابع روی سرور دارید، یک درخواست جدید به سرور ارسال خواهید کرد - نه اینکه به عقب و جلو بروید.
- هنگامی که یک درخواست جدید به سرور ارسال می شود، ابتدا همه سرور کامپوننت ها رندر می شوند، از جمله موارد تودرتو شده در کلاینت کامپوننت ها. نتیجه رندر شده (RSC Payload) حاوی ارجاعاتی به مکان های کلاینت کامپوننت ها خواهد بود. سپس، در سمت کاربر، React از RSC Payload برای تطبیق سرور و کلاینت ها به یک درخت واحد استفاده می کند.
- از آنجایی که کلاینت کامپوننت ها بعد از سرور کامپوننت ها رندر می شوند، نمی توانید یک سرور کامپوننت را به یک module کلاینت کامپوننت وارد کنید (زیرا نیاز به یک درخواست جدید به سرور دارد). در عوض، می توانید یک سرور کامپوننت را به عنوان
propsبه یک کلاینت کامپوننت پاس دهید. بخش های الگوی غیرمجاز و الگوی پشتیبانی شده را در زیر ببینید.
الگوی غیرمجاز: وارد کردن سرور کامپوننت ها به کلاینت کامپوننت ها
الگوی زیر پشتیبانی نمی شود. شما نمی توانید یک سرور کامپوننت را به یک کلاینت کامپوننت وارد کنید:
'use client'
// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}الگوی پشتیبانی شده: ارسال سرور کامپوننت ها به کلاینت کامپوننت ها به عنوان Props
الگوی زیر پشتیبانی می شود. می توانید سرور کامپوننت ها را به عنوان prop به یک کلاینت کامپوننت ارسال کنید.
یک الگوی رایج استفاده از React children prop برای ایجاد یک "slot" در کلاینت کامپوننت شما است.
در مثال زیر، <ClientComponent> یک پراپ children را می پذیرد:
'use client'
import { useState } from 'react'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}<ClientComponent> نمی داند که children در نهایت توسط نتیجه یک سرور کامپوننت پر خواهد شد. تنها مسئولیتی که <ClientComponent> بر عهده دارد این است که تصمیم بگیرد children در نهایت در کجا قرار خواهند گرفت.
در یک سرور کامپوننت والد، می توانید هر دو <ClientComponent> و <ServerComponent> را وارد کنید و <ServerComponent> را به عنوان فرزند <ClientComponent> ارسال کنید:
// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Pages in Next.js are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}با این رویکرد، <ClientComponent> و <ServerComponent> از هم جدا شده اند و می توانند به طور مستقل رندر شوند. در این مورد، فرزند <ServerComponent> می تواند قبل از اینکه <ClientComponent> در سمت کاربر رندر شود، روی سرور رندر شود.
خوب است بدانید:
- الگوی "بالا بردن محتوا" برای جلوگیری از رندر مجدد یک کامپوننت فرزند تودرتو شده در هنگام رندر مجدد یک کامپوننت والد استفاده شده است.
- شما محدود به prop
childrenنیستید. می توانید از هر پراپی برای ارسال JSX استفاده کنید.