ری‌اکت سرور کامپوننت در ۲۰۲۶ — راهنمای عملی برای اپ‌های واقعی

ری‌اکت سرور کامپوننت در ۲۰۲۶ — راهنمای عملی برای اپ‌های واقعی

۱۴۰۵/۱/۱۵
10 دقیقه

بیاموزید React Server Components چگونه کار می‌کنند، کِی از آن‌ها استفاده کنید، و چطور اپ Next.js خود را برای عملکرد بهتر، باندل سبک‌تر و کد تمیزتر ساختار دهید.

خلاصه: React Server Components به شما اجازه می‌دهند به‌صورت پیش‌فرض روی سرور رندر کنید، باندل جاوااسکریپت را کوچک کنید، و بدون سردرگمی useEffect داده واکشی کنید. اگر درست استفاده شود، اپ‌تان سریع‌تر و کدتان تمیزتر می‌شود. اگر اشتباه استفاده شود، سردرگمی ایجاد می‌کند. این راهنما هر دو حالت را پوشش می‌دهد.


Server Components در Next.js 13 به‌صورت پایدار عرضه شدند و سال‌هاست که بالغ شده‌اند — با این حال اکثر توسعه‌دهندگان از روی عادت به "use client" دست می‌زنند و بیشتر بهبودهای عملکردی را از دست می‌دهند.

این سردرگمی قابل درک است. مدل ذهنی واقعاً جدید است:

  • بعضی کامپوننت‌ها فقط روی سرور اجرا می‌شوند
  • بعضی فقط در مرورگر اجرا می‌شوند
  • بعضی می‌توانند روی هر دو اجرا شوند
  • و می‌توانند داخل یکدیگر تو در تو باشند

به محض اینکه این مدل جا بیفتد، بقیه چیزها خودبه‌خود واضح می‌شوند. این پست طراحی شده تا آن اتفاق بیفتد.


یک Server Component یک کامپوننت ری‌اکت است که فقط روی سرور اجرا می‌شود. می‌تواند:

  • مستقیماً از پایگاه داده بخواند
  • متغیرهای محیطی را به‌صورت امن دسترسی داشته باشد
  • کتابخانه‌های سنگین را بدون افزودن به باندل کلاینت ایمپورت کند
  • async باشد و داده را به‌صورت inline await کند

نمی‌تواند:

  • از useState یا useReducer استفاده کند
  • از useEffect استفاده کند
  • event listener اضافه کند
  • به APIهای مختص مرورگر دسترسی داشته باشد

یک Client Component همان چیزی است که ری‌اکت همیشه بوده است. با "use client" در بالای فایل وارد آن می‌شوید.

code
// این یک Server Component است (پیش‌فرض در Next.js App Router)
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id); // فراخوانی مستقیم DB، بدون useEffect
 
  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCartButton productId={product.id} /> {/* Client Component */}
    </main>
  );
}
code
// AddToCartButton.tsx
"use client";
 
import { useState } from "react";
 
export function AddToCartButton({ productId }: { productId: string }) {
  const [added, setAdded] = useState(false);
 
  return (
    <button onClick={() => setAdded(true)}>
      {added ? "اضافه شد!" : "افزودن به سبد خرید"}
    </button>
  );
}

نکته کلیدی: Server Components داده را به‌عنوان props به Client Components پاس می‌دهند. Client Components کوچک و متمرکز بر تعامل می‌مانند.


هر کتابخانه‌ای که داخل یک Server Component ایمپورت می‌کنید به مرورگر ارسال نمی‌شود.

یک صفحه را تصور کنید که تاریخ‌ها را فرمت می‌کند، markdown پارس می‌کند، و کد با syntax highlighting رندر می‌کند. با ری‌اکت سنتی، همه اینها روی کلاینت اجرا می‌شوند. با Server Components، روی سرور می‌ماند. کاربر صفر بایت از آن کتابخانه‌ها را دانلود می‌کند.

قبل از Server Components:

code
باندل صفحه: ~۳۴۰ کیلوبایت gzip شده

بعد از انتقال منطق سنگین به سرور:

code
باندل صفحه: ~۴۸ کیلوبایت gzip شده

این یک بهبود نظری نیست. تفاوت بین صفحه‌ای است که در ۱.۲ ثانیه لود می‌شود و صفحه‌ای که در ۰.۳ ثانیه روی یک اتصال موبایل متوسط لود می‌شود.


۱) داده را تا جایی که استفاده می‌شود واکشی کنید

همه fetch‌های خود را به یک layout سطح بالا منتقل نکنید. جایی که نیاز دارید واکشی کنید.

code
// به جای این — یک fetch بزرگ در بالا
export default async function DashboardPage() {
  const [user, stats, notifications] = await Promise.all([
    getUser(),
    getStats(),
    getNotifications(),
  ]);
 
  return <Dashboard user={user} stats={stats} notifications={notifications} />;
}
code
// این کار را بکنید — هر کامپوننت داده خودش را مدیریت می‌کند
export default function DashboardPage() {
  return (
    <main>
      <UserHeader />         {/* داده کاربر را داخلی واکشی می‌کند */}
      <StatsPanel />         {/* آمار را داخلی واکشی می‌کند */}
      <NotificationsFeed />  {/* اعلان‌ها را داخلی واکشی می‌کند */}
    </main>
  );
}

Next.js فراخوانی‌های fetch را به‌صورت خودکار deduplicate می‌کند. اگر UserHeader و کامپوننت دیگری هر دو getUser() را فراخوانی کنند، فقط یک بار در هر درخواست اجرا می‌شود.


۲) از Suspense برای stream کردن داده‌های کند استفاده کنید

کامپوننت‌های کند را در <Suspense> بپیچید تا بخش‌های سریع صفحه فوراً نمایش داده شوند در حالی که داده‌های کندتر در پس‌زمینه بارگذاری می‌شوند.

code
import { Suspense } from "react";
import { UserHeader } from "@/components/user-header";
import { RecommendationsFeed } from "@/components/recommendations-feed";
import { Skeleton } from "@/components/ui/skeleton";
 
export default function HomePage() {
  return (
    <main>
      <UserHeader /> {/* سریع — فوراً رندر می‌شود */}
 
      <Suspense fallback={<Skeleton className="h-64 w-full" />}>
        <RecommendationsFeed /> {/* کند — وقتی آماده شد stream می‌شود */}
      </Suspense>
    </main>
  );
}

این streaming SSR است. مرورگر HTML اولیه را سریع دریافت می‌کند، آنچه می‌تواند را رندر می‌کند، و بقیه را با رسیدن داده پر می‌کند. هیچ spinner‌ای کل صفحه را بلاک نمی‌کند.


۳) Client Components را در برگ‌های درخت نگه دارید

درخت کامپوننت باید اینطور باشد:

code
Page (Server)
  └─ Layout (Server)
       ├─ Header (Server)
       │    └─ NavMenu (Client) ← تعامل در برگ
       ├─ ArticleContent (Server)
       └─ CommentSection (Client) ← تعامل در برگ

نه اینطور:

code
Page (Client) ← ❌ همه چیز زیر آن حالا client-side است
  └─ Layout (Client)
       ├─ Header (Client)
       └─ ArticleContent (Client)

به محض اینکه یک کامپوننت را "use client" علامت بزنید، همه فرزندان آن هم client-side می‌شوند. تعامل را تا حد ممکن به پایین درخت منتقل کنید.


۴) Server Components را به‌عنوان children به Client Components پاس دهید

این یکی از مفیدترین — و بیشترین سوءتفاهم‌دار — الگوهاست.

code
// Sidebar.tsx — Client Component (برای state باز/بسته نیاز دارد)
"use client";
 
import { useState } from "react";
 
export function Sidebar({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(true);
 
  return (
    <aside className={open ? "w-64" : "w-0"}>
      <button onClick={() => setOpen(!open)}>تغییر وضعیت</button>
      {children}
    </aside>
  );
}
code
// page.tsx — Server Component
import { Sidebar } from "@/components/sidebar";
import { NavLinks } from "@/components/nav-links"; // Server Component
 
export default function Layout() {
  return (
    <Sidebar>
      <NavLinks /> {/* این هنوز یک Server Component است! */}
    </Sidebar>
  );
}

NavLinks یک Server Component است حتی اگر داخل یک Client Component رندر شود. چون به‌عنوان children پاس داده شده، قبلاً روی سرور رندر شده بود قبل از اینکه Sidebar اجرا شود.


۵) از Server Actions برای mutation استفاده کنید

Server Actions به شما اجازه می‌دهند منطق mutation سرور را مستقیماً کنار کامپوننت‌هایتان بنویسید — برای موارد ساده نیازی به API route جداگانه نیست.

code
// app/notes/new/page.tsx
export default function NewNotePage() {
  async function createNote(formData: FormData) {
    "use server";
 
    const title = formData.get("title") as string;
    const content = formData.get("content") as string;
 
    await db.notes.create({ data: { title, content } });
 
    redirect("/notes");
  }
 
  return (
    <form action={createNote}>
      <input name="title" placeholder="عنوان" required />
      <textarea name="content" placeholder="محتوا" required />
      <button type="submit">ذخیره یادداشت</button>
    </form>
  );
}

بدون useState برای فرم. بدون فراخوانی fetch. بدون API route. تابع سرور به‌صورت امن روی سرور اجرا می‌شود و بعد از اتمام redirect می‌کند.

برای فرم‌های پیچیده‌تر، با useActionState برای حالت‌های loading و error ترکیب کنید.


اشتباه ۱ — پاشیدن "use client" همه جا

اگر می‌بینید که به هر فایلی "use client" اضافه می‌کنید، از Server Components استفاده نمی‌کنید. با Client Components به‌عنوان یک انتخاب آگاهانه رفتار کنید، نه یک پیش‌فرض.

اشتباه ۲ — ایمپورت کد server-only داخل Client Components

اگر یک Server Component یک ماژول ایمپورت کند که ماژول دیگری با یک فراخوانی پایگاه داده ایمپورت می‌کند، و آن ماژول در یک Client Component قرار بگیرد — چیزها خراب می‌شوند.

از پکیج server-only برای جلوگیری از ایمپورت‌های تصادفی استفاده کنید:

code
// lib/db.ts
import "server-only";
 
export async function getUsers() {
  return db.users.findMany();
}

حالا اگر این فایل در یک client component ایمپورت شود، یک build error دریافت می‌کنید به جای یک خطای runtime گیج‌کننده.

اشتباه ۳ — سریال‌سازی داده‌های غیرقابل سریال‌سازی در مرز

props‌هایی که از Server Components به Client Components پاس داده می‌شوند باید قابل سریال‌سازی باشند — آبجکت‌های ساده، رشته‌ها، اعداد، آرایه‌ها. نه instance کلاس، نه تابع، نه آبجکت‌های Date.

code
// ❌ این خراب می‌شود
<ClientComponent date={new Date()} />
 
// ✅ این کار می‌کند
<ClientComponent dateString={new Date().toISOString()} />

سناریورویکرد پیشنهادی
واکشی داده از DBServer Component
خواندن متغیرهای محیطیServer Component
رندر محتوای استاتیک یا به‌ندرت تغییرپذیرServer Component
مدیریت UI state (باز/بسته، تب انتخاب‌شده)Client Component
مدیریت ورودی کاربر یا رویدادهای فرمClient Component
دسترسی به browser APIها (localStorage، geolocation)Client Component
ارسال یک mutation فرم سادهServer Action
فرم چند مرحله‌ای پیچیده با اعتبارسنجیClient Component + Server Action
داده کند که نباید صفحه را بلاک کندServer Component داخل Suspense

یک مهاجرت واقعی اپ Next.js از Pages Router به App Router با Server Components معمولاً می‌بیند:

  • ۴۰ تا ۷۰ درصد کاهش در حجم باندل جاوااسکریپت
  • ۳۰ تا ۵۰ درصد بهبود در Time to First Byte برای صفحات داده-محور
  • بهبود قابل توجه در Largest Contentful Paint روی اتصالات کند
  • fetch waterfall کمتر به دلیل هم‌مکانی سمت سرور

این اعداد به شدت به ساختار اپ بستگی دارند. اگر مهاجرت کنید اما "use client" را همه جا نگه دارید، تقریباً هیچ چیزی به دست نمی‌آورید. ساختار است که برنده‌ها را می‌سازد.


code
app/
 ├─ dashboard/
 │   ├─ page.tsx              # Server Component — داده واکشی می‌کند
 │   ├─ stats-panel.tsx       # Server Component — داده خودش را واکشی می‌کند
 │   └─ chart.tsx             # "use client" — نمودار با تعامل رندر می‌کند
 ├─ components/
 │   ├─ server/               # Server Component های خالص
 │   └─ client/               # کامپوننت‌های "use client"
 ├─ lib/
 │   ├─ db.ts                 # server-only — دسترسی به پایگاه داده
 │   └─ utils.ts              # ابزارهای مشترک (برای هر دو امن است)
 └─ actions/
     └─ notes.ts              # Server Actions

جداسازی پوشه‌های server/ و client/ مرز را در نگاه اول قابل مشاهده می‌کند.


React Server Components یک ترند نیستند — یک تغییر اساسی در نحوه ساخت اپ‌های ری‌اکت هستند. آن‌ها پیش‌فرض را سریع می‌کنند به جای اینکه سرعت نیاز به تلاش داشته باشد.

توسعه‌دهندگانی که بیشترین بهره را از آن‌ها می‌برند کسانی هستند که:

  1. مرز server/client را به‌وضوح درک می‌کنند
  2. تعامل را به برگ‌های درخت منتقل می‌کنند
  3. به کامپوننت‌ها اجازه می‌دهند داده‌واکشی خودشان را داشته باشند
  4. از Suspense برای ارسال صفحات اولیه سریع استفاده می‌کنند
  5. برای mutation‌های ساده به Server Actions دست می‌زنند

منحنی یادگیری واقعی اما کوتاه است. به محض اینکه مدل ذهنی روشن شود، می‌بینید که کد کمتری می‌نویسید که کار بیشتری انجام می‌دهد — و صفحاتی ارسال می‌کنید که به‌طور محسوسی سریع‌تر احساس می‌شوند.

این زِن ماجراست.


نوشته پرهام bytezen.dev — راهنماهای عملی برای توسعه‌دهندگانی که به کیفیت اهمیت می‌دهند.

۱۴۰۵/۱/۱۵