Props، انتقال داده و مرز Server/Client

وقتی یک Server Component، یک Client Component را رندر می‌کند، تنها راه رساندن داده به آن Props است. اما این Props فقط داخل یک فرایند جابه‌جا نمی‌شوند؛ از سرور عبور می‌کنند، به‌شکل داده‌ی فشرده درمی‌آیند، داخل پاسخ قرار می‌گیرند و در مرورگر دوباره ساخته می‌شوند. به همین دلیل محدودیت‌هایی دارند: همه‌ی مقدارها قابل ارسال نیستند و هرچه ارسال شود در دسترس مرورگر (و در نتیجه کاربر) قرار می‌گیرد. در این درس این قواعد را می‌شناسیم، خطاهای مربوط را می‌فهمیم و یاد می‌گیریم چطور فقط داده‌ی لازم را عبور بدهیم.

مسیر Props از سرور به مرورگر

وقتی Server Component یک Client Component را با Props رندر می‌کند، این اتفاق می‌افتد:

  1. React روی سرور Props را به قالب سریال‌شده (همان RSC Payload که در درس «مدل کامپوننت‌ها و رندر در App Router» دیدیم) تبدیل می‌کند.
  2. این داده داخل پاسخ قرار می‌گیرد؛ هم داخل سند HTML اولیه و هم در پاسخ‌های ناوبری‌های بعدی.
  3. در مرورگر React از آن، Props کامپوننت را بازسازی می‌کند.

نتیجه‌ی مهم: هرچه به‌عنوان Props به Client Component بدهید، در پاسخ شبکه و سورس صفحه دیده می‌شود، حتی اگر در رابط کاربری نمایش داده نشود.

چه مقدارهایی قابل ارسال هستند؟

React فقط مقدارهایی را می‌تواند سریال کند که به‌صورت داده قابل نمایش باشند. بر اساس مستندات React، مهم‌ترین موارد:

مجاز غیرمجاز
رشته، عدد، bigint، Boolean، null، undefined توابع معمولی (مثل () => ... یا تابع import‌شده)
آرایه، آبجکت ساده (Plain Object) با مقدارهای قابل ارسال نمونه‌ی کلاس‌های سفارشی (new MyClass())
Date، Map، Set، آرایه‌های Typed آبجکت‌هایی با پروتوتایپ تهی (Object.create(null))
Promise (قابل خواندن در Client با use) Symbol غیرعمومی
المان‌های JSX (مثل children) مقدارهایی که به منابع سرور وصل‌اند (مثل اتصال پایگاه داده یا Stream باز)
Server Action (تابعی که با "use server" علامت خورده)

جزئیات فهرست در نسخه‌های React ممکن است کمی گسترش پیدا کند، اما اصل ثابت است: داده می‌شود فرستاد؛ رفتار (تابع و کلاس) نه، مگر Server Action. Server Actionها را در فصل «فرم‌ها و Server Actions» می‌بینیم.

تابع به‌عنوان Prop

متداول‌ترین خطا در این بخش، دادن یک تابع از Server Component به Client Component است:

tsx
// app/example/page.tsx (Server Component)
import Button from "@/components/Button"; // Client Component

export default function Page() {
  function handleClick() {
    console.log("clicked");
  }

  // خطا: توابع نمی‌توانند به Client Component پاس داده شوند
  return <Button onClick={handleClick} />;
}

پیام خطا در حالت توسعه چیزی شبیه این است: «Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server"». دلیلش منطقی است: handleClick روی سرور تعریف شده و نمی‌توان کد دلخواه سرور را به مرورگر فرستاد. راه‌حل‌ها:

  • تابع رویداد را داخل خود Client Component تعریف کنید (معمول‌ترین راه).
  • اگر کار باید روی سرور اجرا شود، آن را Server Action کنید.

نمونه‌ی کلاس

tsx
class Money {
  constructor(public amount: number, public currency: string) {}
}

// خطا: فقط Plain Object قابل ارسال است
<PriceTag price={new Money(100, "USD")} />

// درست: آبجکت ساده
<PriceTag price={{ amount: 100, currency: "USD" }} />

نمونه‌ی کلاس متدهایی دارد که قابل ارسال نیستند. آن را قبل از عبور از مرز به آبجکت ساده تبدیل کنید.

تاریخ و ناهمخوانی در Hydration

Date قابل ارسال است، اما در عمل بهتر است تاریخ را به‌صورت رشته‌ی ISO بفرستید و قالب‌بندی را یک‌جا و یکسان انجام بدهید. دلیل: اگر سرور و مرورگر تاریخ را با منطقه‌ی زمانی یا زبان متفاوت قالب‌بندی کنند، خروجی HTML اولیه با خروجی مرورگر فرق می‌کند و React هشدار ناهمخوانی Hydration می‌دهد. در DevNotes فیلد publishedAt را از ابتدا رشته‌ی ISO نگه داشته‌ایم و فرمت‌کننده‌ی ما منطقه‌ی زمانی را ثابت (UTC) تنظیم کرده است (درس «ساخت فهرست و صفحه‌ی جزئیات نوشته‌ها»).

فقط داده‌ی لازم را بفرستید

چون هر Prop در پاسخ دیده می‌شود، قاعده‌ی مهم این است: فقط فیلدهایی را عبور بدهید که Client Component واقعاً استفاده می‌کند. این هم برای امنیت لازم است و هم برای حجم پاسخ.

مثالی مفهومی (هنوز کاربر و پایگاه داده نساخته‌ایم، پس این کد در پروژه اجرا نمی‌شود):

tsx
// ناامن: کل رکورد کاربر، حتی فیلد حساس، به مرورگر می‌رسد
const user = await getUserById(id); // { id, name, email, passwordHash, ... }
return <UserBadge user={user} />;

// درست: فقط فیلد لازم
return <UserBadge name={user.name} />;

اگر Client Component فقط نام را نشان می‌دهد، فقط name را بدهید. وقتی لایه‌ی داده به پایگاه داده وصل شد (فصل «پایگاه داده با Prisma و SQLite»)، توابع آن را طوری می‌نویسیم که فیلدهای حساس را اصلاً برنگردانند. این الگو را گاهی «DTO» (Data Transfer Object) می‌نامند: یک شکل داده‌ی عمداً کوچک برای عبور از مرز.

یک ابزار هم داریم که در React به‌صورت آزمایشی وجود دارد و می‌تواند نشت اتفاقی مقدارهای مشخص را مسدود کند، اما در این دوره به آن تکیه نمی‌کنیم. روش اصلی ما همان «فقط داده‌ی لازم» و مرز روشن بین لایه‌ها است. در درس «server-only، Context و اشتباهات رایج» ابزار عملی‌تری می‌بینیم.

حجم داده

داده‌ی ارسالی روی اندازه‌ی HTML و پاسخ ناوبری اثر می‌گذارد. دادن آرایه‌ی ۵۰۰ نوشته با بدنه‌ی کامل به یک Client Component فقط برای نمایش عنوان‌ها هر بار حجم زیادی به شبکه تحمیل می‌کند. اگر Client Component فقط عنوان و slug می‌خواهد، فقط همین‌ها را بدهید. بهتر از آن، اگر Client Component به آن فهرست نیاز ندارد، فهرست را در Server Component رندر کنید.

داده در جهت مخالف: از Client به Server

Props فقط از بالا به پایین می‌روند. Client Component نمی‌تواند داده یا مقدار را «به Props والد برگرداند». وقتی Client Component باید چیزی را به سرور بفرستد، سه راه معمول داریم:

روش مناسب برای محل آموزش
تغییر آدرس (Query String و ناوبری) جست‌وجو، فیلتر، صفحه‌بندی (همان کاری که SearchBox می‌کند) درس‌های «searchParams و Query String» و «مسیریابی برنامه‌ای با useRouter»
Server Action ثبت فرم، ایجاد و ویرایش و حذف داده فصل «فرم‌ها و Server Actions»
Route Handler (API) درخواست از مرورگر به یک API یا سرویس خارجی فصل «Route Handlers و API»

children و المان‌های JSX به‌عنوان Props

یک استثنای مهم: خود children (و هر Prop که JSX باشد) قابل ارسال است. این به ما اجازه می‌دهد یک Client Component بسازیم که فقط «پوسته‌ی» تعاملی است و محتوای داخلش را Server Component بسازد. مثلاً Client Component‌ای که یک بخش را باز و بسته می‌کند، ولی محتوای داخل آن روی سرور رندر شده است. این الگو موضوع درس بعد، «ترکیب Server و Client Components»، است و فعلاً فقط بدانید چنین چیزی ممکن است.

عملی: ExpandableText و دیدن Props در پاسخ

یک آزمایش کوتاه انجام می‌دهیم. کامپوننتی می‌سازیم که متن بلند را کوتاه نشان می‌دهد و با دکمه‌ی «ادامه‌ی مطلب» باز می‌کند. چون State دارد، Client Component است. بعد از آزمایش، آن را از صفحه برمی‌داریم، چون برای نوشته‌ی کامل چنین کوتاه‌سازی منطقی نیست.

فایل جدید: components/ExpandableText.tsx

tsx
// components/ExpandableText.tsx
"use client";

import { useState } from "react";

type ExpandableTextProps = {
  text: string;
  maxLength?: number;
};

export default function ExpandableText({
  text,
  maxLength = 200,
}: ExpandableTextProps) {
  const [expanded, setExpanded] = useState(false);

  const isLong = text.length > maxLength;
  const visibleText =
    expanded || !isLong ? text : `${text.slice(0, maxLength).trimEnd()}…`;

  return (
    <>
      <p>{visibleText}</p>
      {isLong && (
        <button
          type="button"
          aria-expanded={expanded}
          onClick={() => setExpanded((prev) => !prev)}
        >
          {expanded ? "نمایش کمتر" : "ادامه‌ی مطلب"}
        </button>
      )}
    </>
  );
}
  • Propها دو مقدار ساده‌اند: یک رشته و یک عدد؛ هر دو کاملاً قابل ارسال.
  • maxLength مقدار پیش‌فرض دارد. Props اختیاری در Server Component هم همین‌طور کار می‌کنند.
  • State داخل کامپوننت است (expanded)، نه در Props؛ Props فقط ورودی هستند و Client Component آن‌ها را تغییر نمی‌دهد.
  • aria-expanded وضعیت باز یا بسته را به صفحه‌خوان‌ها می‌گوید.

استفاده‌ی موقت در صفحه‌ی جزئیات

در app/(public)/posts/[slug]/page.tsx خط <p>{post.body}</p> را موقتاً با این خط جایگزین کنید (و import مربوط را اضافه کنید):

tsx
import ExpandableText from "@/components/ExpandableText";

// ... داخل article:
<ExpandableText text={post.body} maxLength={60} />

رفتار مورد انتظار: متن نوشته بعد از ۶۰ نویسه با «…» بریده می‌شود و دکمه‌ی «ادامه‌ی مطلب» دیده می‌شود. با کلیک، متن کامل نمایش داده می‌شود و دکمه به «نمایش کمتر» تبدیل می‌شود.

دیدن Props در سورس صفحه

در مرورگر روی صفحه کلیک راست و «View Page Source» را بزنید و در سورس دنبال عبارت self.__next_f.push بگردید. متن کامل نوشته را می‌توانید داخل آن اسکریپت‌ها ببینید، حتی وقتی دکمه هنوز بسته است و در صفحه فقط بخشی از متن دیده می‌شود. این نشان می‌دهد Prop text کامل به مرورگر رسیده است. اگر متن حساس بود، همین‌جا افشا می‌شد.

آزمایش خطا: تابع به‌عنوان Prop

حالا خطای مهم این درس را خودتان ببینید. موقتاً در همان صفحه‌ی Server Component یک تابع به ExpandableText بدهید:

tsx
<ExpandableText
  text={post.body}
  maxLength={60}
  onExpand={() => console.log("expanded")}
/>

TypeScript اول خطا می‌دهد چون onExpand در نوع Props وجود ندارد. حتی اگر آن را به نوع اضافه کنید، Next.js هنگام رندر خطای «Functions cannot be passed directly to Client Components» نشان می‌دهد. خطا را مشاهده کنید و سپس این Prop را پاک کنید.

پاک‌سازی

کار تمام شد: خط ExpandableText و import آن را از صفحه بردارید و <p>{post.body}</p> را برگردانید. فایل components/ExpandableText.tsx را هم می‌توانید پاک کنید، چون در DevNotes استفاده نمی‌شود.

راهنمای سریع تصمیم‌گیری

سؤال پاسخ
داده‌ای که می‌دهم رشته، عدد، آرایه یا آبجکت ساده است؟ می‌توان به‌عنوان Prop داد.
می‌خواهم تابع بدهم؟ تابع را داخل Client Component بنویسید، یا Server Action بسازید.
این آبجکت فیلد حساس یا اضافی دارد؟ فقط فیلدهای لازم را بدهید.
تاریخ دارم؟ رشته‌ی ISO بفرستید و قالب‌بندی را ثابت کنید.
Client Component فقط محتوای ثابتی را قاب می‌گیرد؟ محتوا را به‌صورت children بدهید.

نکته‌های مهم

  • Props فقط خواندنی هستند. Client Component نباید Prop را تغییر بدهد؛ اگر لازم است مقدار تغییر کند، آن را کپی کنید و در State نگه دارید.
  • State از Props کپی می‌شود، نه همگام می‌ماند. اگر Prop بعداً عوض شود، State تغییر نمی‌کند (همان مسئله‌ی SearchBox و key).
  • داده‌ی Props یک «عکس لحظه‌ای» است. بعد از رسیدن به مرورگر، تغییر داده در سرور به‌خودی‌خود آن را به‌روز نمی‌کند؛ باید صفحه دوباره رندر یا router.refresh() شود.
  • ارسال کل رکورد وسوسه‌انگیز است. هر بار که می‌نویسید <Something data={bigObject} /> بپرسید: «کدام فیلدها را واقعاً استفاده می‌کند؟»
  • خطاهای سریال‌سازی فقط هنگام رندر ظاهر می‌شوند. TypeScript همه‌ی این محدودیت‌ها را نمی‌تواند تشخیص دهد (مثلاً کلاس سفارشی را با نوع درست می‌پذیرد)؛ پس مسیرهایی که Client Component دارند را در مرورگر امتحان کنید.

جمع‌بندی

  • Props از Server Component به Client Component سریال می‌شوند و داخل پاسخ شبکه و سورس صفحه قابل مشاهده‌اند.
  • مقدارهای ساده، آرایه، آبجکت ساده، Date، JSX و Server Action قابل ارسال‌اند؛ توابع معمولی و نمونه‌ی کلاس‌ها نه.
  • فقط فیلدهای لازم را بدهید تا نشت اطلاعات و حجم اضافی پیش نیاید؛ تاریخ را به‌صورت رشته‌ی ISO و با قالب‌بندی ثابت منتقل کنید.
  • جهت داده فقط از بالا به پایین است؛ برای ارسال به سرور از آدرس، Server Action یا Route Handler استفاده می‌کنیم.
  • در درس بعد، «ترکیب Server و Client Components»، می‌بینیم چگونه با children و الگوهای ترکیبی، مرز Client را کوچک نگه داریم و محتوای سروری را داخل پوسته‌ی کلاینتی قرار دهیم.