تعامل‌پذیری در سمت Client

در درس‌های قبل، مرز Server/Client و محدودیت‌های Serialization را دیدیم. این درس از زاویه‌ای دیگر به همین ساختار نگاه می‌کند: چگونه یک صفحه که بیشتر آن روی سرور ساخته می‌شود، در همان حال تعاملی هم باشد؟ و پیش از اینکه تعامل برقرار شود، بین HTML رسیده و کد جاوااسکریپت چه اتفاقی می‌افتد؟

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

Server Component نمی‌تواند Event Handler یا State داشته باشد، پس هر بخشی که به کلیک، تایپ یا تغییر وضعیت نیاز دارد باید Client Component باشد. الگوی رایج این است که صفحه را به یک بدنه بزرگ و ثابت (روی سرور) و چند بخش کوچک تعاملی (روی Client) تقسیم کنیم. این همان اصل «مرز را تا پایین‌ترین حد ممکن ببرید» درس ۲۴-۲ است.

مثال: دکمه پسندیدن در یک مقاله

jsx
// actions.js
"use server";

export async function toggleLike(postId) {
  await db.likes.toggle(postId);
}
jsx
// LikeButton.js
"use client";

import { useState } from "react";
import { toggleLike } from "./actions";

export default function LikeButton({ postId, initialLiked }) {
  const [liked, setLiked] = useState(initialLiked);

  async function handleClick() {
    setLiked(!liked);
    await toggleLike(postId);
  }

  return (
    <button onClick={handleClick}>
      {liked ? "پسندیده شد" : "پسندیدن"}
    </button>
  );
}
jsx
// Post.js (Server Component)
import LikeButton from "./LikeButton";

export default async function Post({ id }) {
  const post = await db.posts.find(id);
  const liked = await db.likes.hasLiked(id);

  return (
    <article>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
      <LikeButton postId={id} initialLiked={liked} />
    </article>
  );
}

در این ساختار، عنوان و متن مقاله روی سرور خوانده می‌شوند و کدشان به مرورگر نمی‌رسد. فقط LikeButton یک Client Component است. نکته‌های مهم:

  • مقدار اولیه (initialLiked) از Server Component به‌صورت Prop آمده، یک مقدار ساده و قابل Serialization (درس ۲۴-۴).
  • تغییر وضعیت، یک State محلی کلاینت است و بلافاصله دیده می‌شود.
  • برای ذخیره، Client Component یک Server Function را import کرده (از فایلی که با "use server" شروع می‌شود) و آن را صدا می‌زند. این همان Actionهای سمت سرور فصل ۲۳ است.

پیش از Hydration: HTML هست، تعامل نیست

Client Componentها در Frameworkهای رایج ابتدا روی سرور به HTML تبدیل می‌شوند (درس ۲۴-۲). مرورگر این HTML را سریع نشان می‌دهد، ولی هنوز هیچ Event Handler ای وصل نیست. وقتی جاوااسکریپت دانلود و اجرا شد، React در فرایندی به نام Hydration، Handlerها را به همان HTML موجود وصل می‌کند تا صفحه تعاملی شود. بین این دو لحظه، یک فاصله کوتاه هست که صفحه دیده می‌شود ولی کلیک روی دکمه اثری ندارد. هرچه کد Client کمتر باشد، این فاصله کوتاه‌تر است؛ یکی از دلایل اصلی کوچک نگه‌داشتن مرز.

خروجی اولین رندر کلاینت باید با HTML سرور یکی باشد

برای اینکه Hydration درست کار کند، React انتظار دارد اولین رندر Client Component در مرورگر، همان خروجی HTML را بسازد که سرور فرستاده بود. اگر این دو متفاوت باشند، React هشدار عدم تطابق (Hydration Mismatch) می‌دهد و ممکن است بخشی از رابط کاربری از نو ساخته شود. این همان دلیلی است که در درس ۲۳-۱ برای useId دیدیم: مقدار تصادفی در رندر، در سرور و کلاینت متفاوت می‌شود. چند علت رایج:

  • استفاده از Math.random() یا Date.now() مستقیم در بدنه کامپوننت
  • خواندن window یا localStorage در حین Render و نمایش چیزی بر اساس آن
  • نمایش زمان یا منطقه زمانی که روی سرور و مرورگر متفاوت است

«use client» یعنی «فقط در مرورگر اجرا می‌شود»؟ خیر

این یک سوءبرداشت رایج است. چون Client Component روی سرور هم برای پیش‌رندر اجرا می‌شود، نوشتن window.innerWidth مستقیم در بدنه آن، هنگام رندر روی سرور خطا می‌دهد؛ چون window آنجا وجود ندارد. راه درست همان چیزی است که در فصل ششم دیدیم: کدی که به مرورگر وابسته است را در useEffect یا Event Handler بنویسید، چون این دو فقط در مرورگر اجرا می‌شوند:

jsx
"use client";

import { useState, useEffect } from "react";

export default function WindowWidth() {
  const [width, setWidth] = useState(null);

  useEffect(() => {
    setWidth(window.innerWidth);
  }, []);

  return <p>عرض: {width ?? "..."}</p>;
}

اولین رندر روی سرور و مرورگر هر دو "..." نشان می‌دهند و یکسان‌اند؛ مقدار واقعی پس از Hydration در Effect قرار می‌گیرد. برای داده‌ای که از یک Store بیرونی می‌خوانید، آرگومان سوم useSyncExternalStore (getServerSnapshot، درس ۲۳-۲) همین هماهنگی را فراهم می‌کند.

فرم‌ها و Server Function، حتی پیش از Hydration

فرمی که ویژگی action آن یک Server Function باشد، در Frameworkهای پشتیبان می‌تواند حتی پیش از کامل شدن Hydration هم ارسال شود. این یک نمونه Progressive Enhancement است: کاربر با اتصال کند هم می‌تواند فرم را بفرستد. آرگومان سوم permalink در useActionState (درس ۲۳-۶) برای همین سناریو است.

اشتباه رایج: بالا بردن State و Client کردن کل درخت

اگر برای اشتراک State بین دو بخش، آن را به بالاترین کامپوننت ببرید و همان را "use client" کنید، همه زیرمجموعه آن هم Client می‌شوند. راه‌حل، همان الگوی children درس ۲۴-۳ است: Client Component فقط بخش تعاملی را نگه می‌دارد و محتوای ثابت را از والد Server به‌صورت children می‌گیرد.

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

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

جمع‌بندی

در معماری Server/Client، بدنه ثابت روی سرور می‌ماند و فقط بخش‌های کوچک تعاملی Client Component می‌شوند؛ این بخش‌ها می‌توانند Server Function را صدا بزنند. Client Component ابتدا به HTML پیش‌رندر می‌شود و بعد در Hydration تعاملی می‌شود، و اولین رندر آن در مرورگر باید با HTML سرور یکی باشد. "use client" به معنای اجرا فقط در مرورگر نیست؛ کد وابسته به مرورگر باید در Effect یا Event Handler باشد. در درس بعدی، به Data Fetching در معماری Server/Client می‌پردازیم.