server-only، Context و اشتباهات رایج
مرز Server و Client چیزی است که با یک خط "use client" کشیده میشود، اما رعایتکردنش به دقت نیاز دارد. بزرگترین خطر این است که کدی که فقط باید روی سرور بماند (مثل دسترسی به پایگاه داده یا استفاده از رازها) ناخواسته به مرورگر برسد. در این درس با ابزار server-only از این اتفاق جلوگیری میکنیم، میفهمیم Context در معماری Server/Client چگونه کار میکند و خطاهای متداول این مرز را یکییکی میشناسیم. در پایان لایهی دادهی DevNotes را با server-only محافظت میکنیم.
چرا نشت کد سرور خطرناک است؟
هر ماژولی که از یک Client Component import شود، وارد بستهی JavaScript مرورگر میشود (درس «Client Components و دستور use client»). اگر آن ماژول کد پایگاه داده، منطق داخلی یا مقدارهای محرمانه داشته باشد، اینها در اختیار هر کاربری قرار میگیرند که ابزار توسعهدهندهی مرورگر را باز کند. مشکل این است که این اتفاق ممکن است بیصدا رخ دهد؛ مثلاً کسی در آینده یک تابع کمکی را از lib/posts.ts داخل یک Client Component import کند و هیچ خطایی نبیند.
بستهی server-only
بستهی server-only یک محافظ ساده و مؤثر است. فقط کافی است آن را در بالای فایلی import کنید که نباید به مرورگر برسد. اگر بعداً هر Client Component (مستقیم یا غیرمستقیم) آن فایل را import کند، Build یا اجرا با خطا متوقف میشود و بهجای نشت بیصدا، همان لحظه مطلع میشوید.
npm install server-only// lib/example.ts
import "server-only";
export async function getSecretReport() {
// کد حساس سمت سرور
}- این import فقط بهعنوان نشانه است؛ چیزی از آن استفاده نمیکنید و نامی هم نمیگیرد.
- بستهی همتای آن
client-onlyاست برای ماژولهایی که فقط باید در مرورگر استفاده شوند (مثلاً وابسته بهwindow). کاربردش کمتر است و در این دوره به آن نیاز نداریم. server-onlyمانع نوشتن کد نادرست نمیشود؛ فقط ورود آن به گراف مرورگر را قطع میکند و پیام خطا میدهد.
عملی: محافظت از لایهی دادهی DevNotes
فایل lib/posts.ts لایهی دسترسی به داده است و در فصل «پایگاه داده با Prisma و SQLite» به پایگاه داده وصل میشود. از همین الان آن را محافظت میکنیم تا عادت درستی شکل بگیرد.
فایلهای متأثر: package.json (با نصب بسته) و lib/posts.ts (یک خط در ابتدا).
ابتدا بسته را نصب کنید:
npm install server-only
سپس بالاترین خط lib/posts.ts را به این شکل کنید (بقیهی فایل بدون تغییر میماند):
// lib/posts.ts
import "server-only";
// لایهی موقت داده برای آموزش؛ در محیط Production مناسب نیست.
// داده در حافظهی برنامه است و هیچچیز ذخیره نمیشود.
// در فصل «پایگاه داده با Prisma و SQLite» پیادهسازی با Prisma جایگزین میشود.
export type Post = {
slug: string;
title: string;
excerpt: string;
body: string;
publishedAt: string;
};
// ... بقیهی فایل بدون تغییر
حالا پروژه را بررسی کنید. تمام استفادههای فعلی از این فایل در Server Componentها هستند (صفحهها، LatestPosts، RelatedPosts)، پس همهچیز بدون تغییر کار میکند. یک نکته: PostCard از lib/posts فقط نوع Post را با import type میگیرد و چون نوعها هنگام کامپایل حذف میشوند، مشکلی ایجاد نمیشود. اگر Client Componentی هم بخواهد فقط نوع را استفاده کند، با import type مجاز است.
آزمایش: دیدن خطای محافظ
برای اینکه مطمئن شوید محافظ کار میکند، یک نشت عمدی بسازید. اینها فقط موقتی هستند.
- فایل موقت
components/LeakyClient.tsxرا بسازید:
// components/LeakyClient.tsx (فقط برای آزمایش؛ بعداً حذف میشود)
"use client";
import { getAllPosts } from "@/lib/posts";
export default function LeakyClient() {
console.log(typeof getAllPosts);
return <p>آزمایش</p>;
}- آن را موقتاً در
app/(public)/about/page.tsxرندر کنید (با import و<LeakyClient />). - صفحهی
/aboutرا باز کنید.
رفتار مورد انتظار: Next.js خطایی شبیه «You're importing a component that needs "server-only". That only works in a Server Component but one of its parents is marked with "use client"...» نشان میدهد و ماژول را به مرورگر نمیرساند. اگر server-only نبود، بدون هیچ پیامی کد لایهی داده در بستهی مرورگر قرار میگرفت.
بعد از دیدن خطا، LeakyClient را از about/page.tsx بردارید و فایل components/LeakyClient.tsx را حذف کنید.
الگوی عملی
قرارداد سادهای که از این به بعد در DevNotes رعایت میکنیم: هر فایلی در lib که به پایگاه داده، نشست کاربر، رازها یا منطق سروری دسترسی دارد، با import "server-only" شروع شود. فایلهایی مثل lib/format.ts که توابع خالص هستند و هر دو سمت میتوانند استفاده کنند، این خط را نمیگیرند.
متغیرهای محیطی و Client Component
متغیرهای محیطی هم از همین قاعده پیروی میکنند، اما با سازوکار دیگری. در Next.js فقط متغیرهایی که نامشان با NEXT_PUBLIC_ شروع شود هنگام Build داخل کد مرورگر قرار میگیرند. بقیه فقط در سرور در دسترساند و در Client Component مقدار undefined دارند. دو نتیجه:
- هرگز برای مقدار محرمانه (کلید API، رمز پایگاه داده) از پیشوند
NEXT_PUBLIC_استفاده نکنید؛ آن مقدار برای همه قابل مشاهده است. - اگر در Client Component مقدار یک متغیر بدون پیشوند را بخوانید، خطا نمیگیرید بلکه
undefinedمیگیرید؛ این معمولاً نشانهی این است که آن کد باید روی سرور باشد.
جزئیات پیکربندی را در درسهای «متغیرهای محیطی و فایلهای .env» و «متغیرهای عمومی و محرمانه» میبینیم.
Context در Server و Client Components
Context در React روشی برای رساندن یک مقدار به چند کامپوننت عمیق بدون Props دادن مرحلهبهمرحله است. در App Router قواعد زیر را دارد:
createContextوuseContextفقط در Client Component کار میکنند. Server Component نمیتواند Context بسازد یا مصرف کند.- Provider (کامپوننتی که مقدار را فراهم میکند) باید خودش Client Component باشد.
- Server Component میتواند Provider را رندر کند و محتوا را بهصورت
childrenبه آن بدهد؛ همان الگوی درس «ترکیب Server و Client Components». - تنها کامپوننتهای Client که داخل Provider هستند به مقدار Context دسترسی دارند. Server Componentهایی که بهعنوان
childrenداده شدهاند آن را نمیبینند.
نمونهی یک Provider
این یک نمونهی مفهومی است و در DevNotes فعلاً به Context نیاز نداریم، پس ساخته نمیشود. اما الگوی درست آن را بدانید:
// components/ThemeProvider.tsx (نمونهی مفهومی)
"use client";
import {
createContext,
useContext,
useState,
type ReactNode,
} from "react";
type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Theme;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
function toggleTheme() {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
}
return (
<ThemeContext value={{ theme, toggleTheme }}>
{children}
</ThemeContext>
);
}
export function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme باید داخل ThemeProvider استفاده شود.");
}
return context;
}- همهی فایل Client است، چون هم
createContextو همuseStateدارد. - در React 19 میتوان خود Context را مستقیم بهعنوان Provider رندر کرد (
<ThemeContext value=...>)؛ شکل قدیمی<ThemeContext.Provider>هم هنوز کار میکند. - مقدار پیشفرض
nullو بررسی داخلuseThemeباعث میشود استفادهی خارج از Provider با پیام روشن خطا بدهد، نه با یکundefinedمبهم. - تابع
toggleThemeدرون خود ماژول Client ساخته شده و از مرز Server/Client عبور نمیکند؛ پس محدودیت «تابع بهعنوان Prop» اینجا مشکلی ایجاد نمیکند.
استفاده در Layout
// app/layout.tsx (نمونهی مفهومی)
import { ThemeProvider } from "@/components/ThemeProvider";
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="fa" dir="rtl">
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
RootLayout Server Component میماند و صفحهها (که ممکن است Server باشند) درون Provider رندر میشوند. فقط خودِ Provider و مصرفکنندههای Context در مرورگر اجرا میشوند.
توصیهی عملی: Provider را تا جای ممکن پایین در درخت بگذارید. مثلاً اگر فقط یک بخش از سایت به Context نیاز دارد، Provider را در Layout همان بخش بگذارید، نه Root Layout. این کار به Next.js اجازه میدهد بخشهای سروری بالاتر را بهتر بهینه کند.
خطاهای متداول در مرز Server/Client
این فهرست خطاهایی است که بیشتر از همه پیش میآید، همراه با نشانهی هر خطا و راه رفع:
۱. استفاده از Hook یا رویداد در Server Component
نشانه: پیام خطا دربارهی useState، useEffect یا «Event handlers cannot be passed to Client Component props». راه رفع: بخش تعاملی را در یک Client Component کوچک جدا کنید (درس «Client Components و دستور use client»).
۲. "use client" بالاتر از حد لازم
نشانه: صفحهای که async بود نمیتواند داده بخواند، یا metadata کار نمیکند. راه رفع: مرز را به برگها ببرید و محتوای سروری را با children داخل پوستهی Client بدهید (درس «ترکیب Server و Client Components»).
۳. import کردن کد سروری در Client Component
نشانه: خطای «server-only» (اگر محافظ را گذاشتهاید)، یا خطاهایی مثل «Module not found: Can't resolve 'fs'» وقتی کتابخانهی مخصوص Node در مرورگر import شده است. راه رفع: داده را در Server Component بخوانید و بهصورت Props بدهید؛ فایلهای سروری را با server-only علامت بزنید.
۴. دادن تابع یا نمونهی کلاس بهعنوان Prop
نشانه: «Functions cannot be passed directly to Client Components». راه رفع: درس «Props، انتقال داده و مرز Server/Client».
۵. نشت دادهی اضافی از طریق Props
نشانه: بدون خطا؛ فقط در سورس صفحه یا پاسخ شبکه فیلدهای اضافی دیده میشوند. راه رفع: فقط فیلدهای لازم را بدهید.
۶. استفاده از Context در Server Component
نشانه: خطای «createContext only works in Client Components». راه رفع: Provider را در یک فایل "use client" بسازید و در Layout با children رندر کنید.
۷. Provider بالاتر یا خارج از محدودهی مصرفکننده
نشانه: useContext مقدار پیشفرض (null) میگیرد یا خطای سفارشی ما مثل «باید داخل ThemeProvider استفاده شود» دیده میشود. راه رفع: مصرفکننده را داخل Provider قرار دهید.
۸. کتابخانهی شخص ثالث بدون "use client"
بعضی کتابخانهها از Hookها یا APIهای مرورگر استفاده میکنند ولی دستور "use client" ندارند. وقتی در Server Component از آنها استفاده کنید، خطا میگیرید. راه رفع: یک پوشش (Wrapper) Client بسازید:
// components/Carousel.tsx (نمونهی مفهومی)
"use client";
import { Carousel } from "some-carousel-library";
export default Carousel;
حالا میتوانید components/Carousel را در Server Component رندر کنید. نام کتابخانه فرضی است؛ الگو مهم است.
۹. ناهمخوانی Hydration
نشانه: هشدار «Hydration failed because the server rendered HTML didn't match the client». علت متداول: استفاده از Math.random()، Date.now()، window یا قالببندی تاریخ با منطقهی زمانی متفاوت در بدنهی رندر. راه رفع: مقدار را در useEffect یا رویداد بگیرید، یا قالببندی را ثابت کنید (همان کاری که با timeZone: "UTC" در formatDate کردیم).
۱۰. async کردن Client Component
نشانه: خطای «async/await is not yet supported in Client Components». راه رفع: داده را در Server Component بخوانید و Props بدهید؛ یا در Client با Hookها و روشهایی که در درس «دریافت داده در Client Components و موارد استفاده» میآید.
چکلیست قبل از Clientکردن یک کامپوننت
| سؤال | اگر جواب «نه» است |
|---|---|
| واقعاً به State، Effect، رویداد یا API مرورگر نیاز دارم؟ | Server Component بماند. |
| میتوانم فقط بخش تعاملی را جدا کنم؟ | اگر بله، فقط همان بخش را Client کن. |
| این فایل و importهایش چیز حساس یا سنگین سروری دارند؟ | اگر بله، آنها را به Server بسپار و server-only بگذار. |
| Propsی که میدهم قابل ارسال و حداقلی هستند؟ | اگر نه، اصلاحشان کن. |
جمعبندی
- هر ماژولی که از Client Component import شود به مرورگر میرود؛ برای جلوگیری از نشت کد سروری،
import "server-only"را در ابتدای آن فایل بگذارید تا import اشتباه خطا بدهد. - فقط متغیرهای محیطی با پیشوند
NEXT_PUBLIC_به مرورگر میرسند؛ مقدار محرمانه هرگز نباید این پیشوند را بگیرد. - Context فقط در Client Component ساخته و مصرف میشود؛ Provider را در یک فایل Client میسازیم، با
childrenدر Layout رندر میکنیم و تا جای ممکن پایین در درخت میگذاریم. - خطاهای متداول مرز Server/Client شامل Hook در Server،
"use client"بالاتر از حد لازم، import کد سروری در Client، Props نامعتبر، Context در Server، کتابخانهی بدون"use client"، ناهمخوانی Hydration وasyncدر Client هستند. - در DevNotes اکنون
lib/posts.tsباserver-onlyمحافظت شده است. - در درس بعد، «انتخاب محل اجرای کد و کاهش JavaScript سمت کلاینت»، آنچه در این فصل یاد گرفتیم را برای تصمیمگیری دربارهی محل اجرای هر بخش و کمکردن حجم JavaScript به کار میبریم.
