Data Fetching در معماری Server/Client

در فصل چهاردهم، دریافت داده را با useEffect، State برای loading و error، محافظت در برابر Race Condition و Caching ساختیم. در معماری Server/Client، چند تا از این مشکلات از ریشه عوض می‌شوند. این درس نشان می‌دهد داده را کجا باید گرفت، چه چیزی ساده‌تر می‌شود و چه چیزی هنوز باید مراقبش بود.

پیش‌فرض جدید: دریافت داده در Server Component

همان‌طور که در درس ۲۴-۱ دیدیم، Server Component می‌تواند async باشد. بنابراین جای طبیعی دریافت داده برای محتوای صفحه، خود Server Component است:

jsx
async function Post({ id }) {
  const post = await db.posts.find(id);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

چه چیزی ساده‌تر می‌شود

  • بدون useEffect و State برای loading: داده پیش از ساخته شدن خروجی خوانده می‌شود، پس کامپوننت همیشه با داده کامل رندر می‌شود.
  • بدون Race Condition: هر درخواست یک رندر روی سرور است و مسئله «پاسخ قدیمی دیرتر از جدید برسد» (فصل ۶ و ۱۴) در این الگو پیش نمی‌آید.
  • دسترسی مستقیم به منبع داده: می‌توان از پایگاه داده یا یک سرویس داخلی، بدون ساختن یک API جداگانه، خواند. کلیدهای محرمانه هم روی سرور می‌مانند (درس ۲۴-۱).
  • کاهش رفت‌وبرگشت شبکه: درخواست‌های داده از سرور به منبع داده، معمولاً سریع‌تر از درخواست از مرورگر کاربر انجام می‌شوند.

مشکل جدید: آبشار درخواست‌ها (Waterfall)

await پشت‌سرهم، درخواست‌ها را ترتیبی می‌کند، حتی اگر مستقل از هم باشند:

jsx
// ترتیبی: درخواست دوم تا پایان اولی شروع نمی‌شود
async function Page({ id }) {
  const user = await getUser(id);
  const posts = await getPosts(id);
  return <Profile user={user} posts={posts} />;
}

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

jsx
async function Page({ id }) {
  const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);
  return <Profile user={user} posts={posts} />;
}

آبشار فقط در یک تابع نیست: اگر یک Server Component والد، داده‌اش را await کند و بعد فرزندی را رندر کند که خودش await دارد، رندر فرزند تا پایان درخواست والد شروع نمی‌شود. اگر داده‌ی فرزند به داده‌ی والد وابسته نیست، بهتر است هر دو درخواست زودتر و هم‌زمان شروع شوند.

جلوگیری از درخواست تکراری با cache

گاهی چند کامپوننت در یک صفحه به همان داده نیاز دارند (مثلاً هم عنوان صفحه و هم بدنه، کاربر فعلی را می‌خواهند). React برای Server Components تابعی به نام cache دارد که نتیجه یک تابع را در طول رندر همان درخواست به خاطر می‌سپارد، پس چند فراخوانی با آرگومان یکسان، فقط یک‌بار اجرا می‌شود:

jsx
import { cache } from "react";

export const getUser = cache(async (id) => {
  return db.users.find(id);
});

این یک کش در سطح همان رندر سرور است، نه کش بین درخواست‌ها یا کش مرورگر. Frameworkها لایه‌های کش بلندمدت‌تر و روش‌های ابطال آن را جداگانه ارائه می‌دهند و جزئیاتشان به Framework بستگی دارد.

انتقال Promise به Client Component

هر داده‌ای لازم نیست پیش از ارسال صفحه آماده شود. می‌توان یک درخواست را شروع کرد، بدون await آن را به یک Client Component داد، و در آنجا با use (درس ۲۳-۸) خواند. بخش‌های سریع‌تر صفحه بلافاصله نمایش داده می‌شوند و بخش کندتر با Suspense (فصل ۲۱) منتظر می‌ماند:

jsx
// Page.js (Server Component)
import { Suspense } from "react";
import Comments from "./Comments";

export default async function Page({ id }) {
  const post = await getPost(id);
  const commentsPromise = getComments(id);

  return (
    <article>
      <h1>{post.title}</h1>
      <Suspense fallback={<p>در حال بارگذاری نظرات...</p>}>
        <Comments commentsPromise={commentsPromise} />
      </Suspense>
    </article>
  );
}
jsx
// Comments.js
"use client";

import { use } from "react";

export default function Comments({ commentsPromise }) {
  const comments = use(commentsPromise);

  return (
    <ul>
      {comments.map((c) => (
        <li key={c.id}>{c.text}</li>
      ))}
    </ul>
  );
}

این الگو همان مشکل «Promise باید پایدار باشد» درس ۲۳-۸ را حل می‌کند: Promise یک‌بار در Server Component ساخته می‌شود و به‌عنوان Prop قابل انتقال (درس ۲۴-۴) پایین می‌رود، نه اینکه در هر رندر Client دوباره ساخته شود.

هنوز کجا دریافت داده سمت Client لازم است

Server Component یک‌بار رندر می‌شود، پس برای داده‌ای که به تعامل مداوم در مرورگر وابسته است، کافی نیست. مثال‌ها: جست‌وجوی هم‌زمان با تایپ، Polling دوره‌ای، یا داده‌ای که باید پس از هر تغییر در مرورگر دوباره گرفته شود. برای این موارد، همچنان همان ابزارهای فصل چهاردهم و شانزدهم (کتابخانه‌های Server State در Client Component) مناسب‌اند. این دو رویکرد در یک برنامه کنار هم می‌آیند: داده اولیه صفحه از Server Component، و داده تعاملی از Client Component.

خطا و Loading

اگر دریافت داده در Server Component با خطا مواجه شود، طبق فصل هفدهم، Error Boundary نزدیک آن را می‌گیرد. چون Error Boundary یک کامپوننت کلاسی با State است، باید در یک ماژول "use client" باشد؛ بسیاری از Frameworkها برای همین، فایل‌های قراردادی ارائه می‌دهند. برای حالت Loading هم همان Suspense فصل بیست‌ویکم، به‌جای State دستی loading، کافی است. بررسی بیشتر آن در درس بعدی، Streaming، می‌آید.

تغییر داده: Server Function، نه fetch در Effect

برای ارسال تغییر به سرور (ذخیره، حذف، به‌روزرسانی)، همان Server Function و Actionهای فصل ۲۳ به کار می‌روند. اینکه پس از تغییر، Server Componentها چگونه دوباره اجرا شوند و داده تازه نمایش داده شود، به مکانیزم ابطال کش هر Framework وابسته است و از موضوع این دوره خارج است.

چرا این درس مثال قابل اجرا ندارد

دریافت داده در Server Component و Streaming نیاز به یک Framework با Server Components دارد که در ویرایشگر درون‌صفحه‌ای در دسترس نیست. کدها را در یک پروژه واقعی اجرا کنید.

جمع‌بندی

در معماری Server/Client، داده اولیه صفحه معمولاً در Server Component با async/await گرفته می‌شود، که نیاز به useEffect، State برای loading و محافظت در برابر Race Condition را حذف می‌کند. باید مراقب آبشار درخواست‌ها بود و درخواست‌های مستقل را هم‌زمان (مثلاً با Promise.all) شروع کرد؛ cache از تکرار فراخوانی در یک رندر جلوگیری می‌کند. داده کندتر را می‌توان به‌صورت Promise به Client Component داد و با use و Suspense خواند. داده وابسته به تعامل مداوم مرورگر هنوز با ابزارهای سمت Client مدیریت می‌شود. در درس بعدی، به Streaming می‌پردازیم.