Client Components و دستور use client

در درس «Server Components و کاربرد آن‌ها» دیدیم که Server Component چه کارهایی می‌تواند و چه کارهایی نمی‌تواند انجام دهد. هرچه Server Component نمی‌تواند (State، رویداد، APIهای مرورگر) دقیقاً کار Client Component است. در این درس یاد می‌گیریم دستور "use client" دقیقاً چه می‌کند، مرز Server و Client کجا کشیده می‌شود، چه چیزهایی تحت تأثیر این مرز قرار می‌گیرند و چطور این مرز را کوچک و در جای درست نگه داریم. در پایان یک دکمه‌ی «کپی لینک» برای صفحه‌ی نوشته می‌سازیم.

Client Component چیست؟

Client Component کامپوننتی است که کدش به مرورگر فرستاده می‌شود و در آنجا اجرا می‌شود. برای ساختنش فقط باید خط "use client" را در ابتدای فایل بنویسید. چنین کامپوننتی:

  • می‌تواند useState، useEffect و بقیه‌ی Hookهای تعاملی React را استفاده کند.
  • می‌تواند رویدادهایی مثل onClick و onChange را مدیریت کند.
  • می‌تواند از APIهای مرورگر مثل window، navigator و localStorage استفاده کند.
  • می‌تواند Hookهای مخصوص Next.js مثل useRouter، usePathname و useSearchParams را به کار ببرد.
  • نمی‌تواند async باشد.

یادآوری از درس «مدل کامپوننت‌ها و رندر در App Router»: Client Component فقط در مرورگر اجرا نمی‌شود. برای HTML اولیه، یک‌بار هم روی سرور رندر می‌شود و بعد در مرورگر Hydrate می‌شود. به همین دلیل کد آن باید هر دو محیط را تحمل کند.

قواعد دستور "use client"

tsx
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}
قاعده توضیح
جایگاه باید در بالاترین نقطه‌ی فایل و قبل از هر import باشد. فقط کامنت می‌تواند قبل از آن بیاید.
نوشتار یک رشته‌ی متنی است؛ "use client" یا 'use client' هر دو درست‌اند. بدون رشته (مثلاً فقط use client) یا با خطای تایپی اثری ندارد و بی‌صدا نادیده گرفته می‌شود.
دامنه‌ی اثر روی کل فایل و تمام ماژول‌هایی که این فایل import می‌کند اثر می‌گذارد.
فایل‌های زیرمجموعه لازم نیست در هر فایلی که از Client Component import می‌شود دوباره "use client" بنویسید؛ همه‌ی آن‌ها در بسته‌ی مرورگر قرار می‌گیرند.
تعداد فقط جایی که مرز شروع می‌شود لازم است؛ نه در همه‌ی فایل‌ها.

مرز Server/Client: دو گراف ماژول

Next.js پروژه‌ی شما را به‌صورت دو گراف از ماژول‌ها می‌بیند: یکی برای سرور و یکی برای مرورگر. دستور "use client" نقطه‌ای را مشخص می‌کند که از آنجا به بعد (به سمت فایل‌هایی که import می‌شوند) ماژول‌ها وارد گراف مرورگر می‌شوند:

text
PostPage (Server)
├── BackButton (Client)         ← "use client"؛ مرز اینجاست
│   └── useRouter ...           ← همه‌ی importهای این فایل هم Client‌اند
└── CopyLinkButton (Client)     ← مرز دوم
    └── useState ...

یک نکته‌ی ظریف: "use client" درباره‌ی «ماژول‌هایی که import می‌کنید» است، نه «کامپوننت‌هایی که رندر می‌کنید». یعنی اگر یک Client Component مستقیماً فایل دیگری را import کند، آن فایل هم Client می‌شود. اما اگر یک Server Component، یک Client Component را رندر کند و Server Component دیگری را به‌صورت children به آن بدهد، آن فرزند Server می‌ماند. این الگوی مهم را در درس «ترکیب Server و Client Components» می‌بینیم.

مرز را در بالا نگذارید

یک اشتباه رایج در شروع کار این است که چون یک بخش کوچک به State نیاز دارد، کل صفحه را با "use client" علامت بزنند. اثر این کار:

  • همه‌ی کامپوننت‌هایی که آن صفحه import می‌کند (کارت‌ها، لایه‌ی داده و غیره) وارد بسته‌ی مرورگر می‌شوند.
  • صفحه دیگر نمی‌تواند async باشد و نمی‌تواند مستقیم داده بخواند.
  • در فایل Client نمی‌توان metadata یا generateStaticParams را export کرد.
  • JavaScript بیشتری به مرورگر می‌رود و صفحه دیرتر تعاملی می‌شود.

راه درست: بخش تعاملی را در یک کامپوننت کوچک جدا کنید و فقط همان را Client کنید، همان‌طور که با NavLink، SearchBox و BackButton کرده‌ایم. به این کار «انتقال مرز به برگ‌ها» می‌گوییم.

چه چیزهایی از Server Component به Client Component می‌رسند؟

  • Props: فقط مقادیر قابل ارسال (Serializable) مجازند: رشته، عدد، Boolean، آرایه، آبجکت ساده، null و undefined. توابع معمولی و کلاس‌ها مجاز نیستند. بررسی کامل در درس «Props، انتقال داده و مرز Server/Client» است.
  • کد ماژول‌ها: هر ماژولی که از داخل Client Component import شود به مرورگر می‌رود. پس کدی که فقط باید روی سرور بماند (مثل اتصال به پایگاه داده) هرگز نباید از Client Component import شود. ابزار محافظت از آن را در درس «server-only، Context و اشتباهات رایج» می‌بینیم.

رفتار روی سرور و محدودیت window

چون Client Component برای HTML اولیه روی سرور هم رندر می‌شود، در بدنه‌ی رندر نمی‌توان مستقیم به window یا localStorage دست زد؛ روی سرور وجود ندارند و خطای window is not defined می‌دهند:

tsx
"use client";

export default function Width() {
  // خطا روی سرور: window تعریف نشده است
  const width = window.innerWidth;

  return <p>{width}</p>;
}

راه‌حل این است که کد وابسته به مرورگر را در جایی بگذاریم که فقط در مرورگر اجرا می‌شود: داخل تابع رویداد (مثل onClick) یا داخل useEffect. موضوع Hookها را در درس بعد، «State، Event و Hookها»، می‌بینیم.

عملی: دکمه‌ی «کپی لینک» برای نوشته

می‌خواهیم در صفحه‌ی نوشته دکمه‌ای بگذاریم که آدرس همان نوشته را در کلیپ‌بورد کپی کند. این کار به رویداد کلیک و navigator.clipboard (API مرورگر) نیاز دارد، پس باید Client Component باشد. برای نشان‌دادن بازخورد هم از یک State ساده استفاده می‌کنیم؛ جزئیات State و Hookها در درس بعد می‌آید.

فایل‌های متأثر: components/CopyLinkButton.tsx (جدید) و app/(public)/posts/[slug]/page.tsx (تغییر).

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

import { useState } from "react";

type CopyLinkButtonProps = {
  path: string;
};

export default function CopyLinkButton({ path }: CopyLinkButtonProps) {
  const [copied, setCopied] = useState(false);

  async function handleClick() {
    try {
      await navigator.clipboard.writeText(`${window.location.origin}${path}`);
      setCopied(true);
      window.setTimeout(() => setCopied(false), 2000);
    } catch {
      setCopied(false);
    }
  }

  return (
    <>
      <button type="button" onClick={handleClick}>
        کپی لینک نوشته
      </button>
      <span role="status">{copied ? " لینک کپی شد." : ""}</span>
    </>
  );
}

توضیح کد

  • "use client" در بالای فایل، قبل از importها.
  • prop به نام path فقط رشته است (قابل ارسال از Server)؛ آدرس کامل را داخل مرورگر با window.location.origin می‌سازیم، چون Server Component آدرس دامنه‌ی کاربر را نمی‌داند.
  • window و navigator فقط داخل handleClick استفاده شده‌اند که فقط هنگام کلیک در مرورگر اجرا می‌شود؛ پس رندر روی سرور مشکلی ندارد.
  • navigator.clipboard فقط در بستر امن (HTTPS یا localhost) کار می‌کند. try/catch هم برای وقتی است که مرورگر اجازه‌ی دسترسی ندهد.
  • عنصر role="status" باعث می‌شود صفحه‌خوان‌ها پیام «لینک کپی شد.» را اعلام کنند.
  • <>...</> یک Fragment است تا دکمه و پیام بدون تگ اضافی کنار هم باشند.

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

tsx
// app/(public)/posts/[slug]/page.tsx
import Link from "next/link";
import { notFound } from "next/navigation";
import BackButton from "@/components/BackButton";
import CopyLinkButton from "@/components/CopyLinkButton";
import { formatDate } from "@/lib/format";
import { getAllPosts, getPostBySlug } from "@/lib/posts";

type PostPageProps = {
  params: Promise<{ slug: string }>;
};

export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function PostPage({ params }: PostPageProps) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  if (!post) {
    notFound();
  }

  return (
    <main>
      <BackButton />
      <article>
        <h1>{post.title}</h1>
        <p>
          <time dateTime={post.publishedAt}>
            {formatDate(post.publishedAt)}
          </time>
        </p>
        <p>{post.body}</p>
        <CopyLinkButton path={`/posts/${post.slug}`} />
      </article>
      <p>
        <Link href="/posts">همه‌ی نوشته‌ها</Link>
      </p>
    </main>
  );
}

صفحه Server Component می‌ماند: async است، داده می‌خواند و generateStaticParams را export می‌کند. فقط دو کامپوننت کوچک (BackButton و CopyLinkButton) به مرورگر می‌روند.

خروجی و رفتار مورد انتظار

  1. /posts/hello-nextjs را باز کنید؛ دکمه‌ی «کپی لینک نوشته» زیر متن دیده می‌شود.
  2. روی دکمه کلیک کنید؛ پیام «لینک کپی شد.» نشان داده می‌شود و بعد از حدود دو ثانیه ناپدید می‌شود.
  3. در یک تب جدید، متن کپی‌شده را بچسبانید؛ آدرس کامل (مثل http://localhost:3000/posts/hello-nextjs) می‌آید.

آزمایش: حذف "use client"

  1. خط "use client" را موقتاً از اول CopyLinkButton.tsx پاک کنید.
  2. صفحه‌ی نوشته را ریفرش کنید.
  3. Next.js خطایی نشان می‌دهد که useState یا رویداد onClick در Server Component مجاز نیست و پیشنهاد می‌کند "use client" اضافه شود.
  4. خط را برگردانید تا خطا برطرف شود.

این آزمایش نشان می‌دهد Next.js وضعیت «فراموش‌کردن مرز» را می‌شناسد و پیام راهنما می‌دهد.

کِی Client Component لازم است؟

نیاز مثال در DevNotes
State و تعامل با کاربر SearchBox و CopyLinkButton
رویدادهای مرورگر کلیک دکمه‌ها
APIهای مرورگر navigator.clipboard
Hookهای مسیر NavLink (usePathname) و BackButton (useRouter)
Hookها و کتابخانه‌های وابسته به State فرم‌های تعاملی در فصل‌های بعد

اگر کامپوننتی هیچ‌کدام از این نیازها را ندارد، آن را Client نکنید.

نکته‌های مهم

  • کامپوننت Client باید کوچک باشد: هر خط کد داخل آن (و همه‌ی importهایش) به مرورگر می‌رود.
  • تابع async برای Client Component ممنوع است: داده را در Server Component بخوانید و به‌عنوان Props بدهید، یا در Client از Hookهای مناسب استفاده کنید.
  • خروجی سرور و مرورگر باید یکسان باشد: استفاده از Math.random() یا Date.now() در بدنه‌ی رندر باعث ناهمخوانی Hydration می‌شود. مقدار تصادفی یا زمان‌بندی‌شده را داخل Effect یا رویداد بگیرید.
  • Client بودن به معنی امن‌بودن یا ناامن‌بودن نیست، ولی کد آن عمومی است: هر چه در Client Component باشد، کاربر می‌تواند در ابزار توسعه‌دهنده ببیند. هیچ رمز و کلیدی را در آن قرار ندهید.
  • فقط use client کافی نیست: Server Component هنوز می‌تواند Client Component را رندر کند و Props بدهد. وابستگی به صدا زدن توابع Client از سمت Server وجود ندارد؛ فقط می‌توان آن‌ها را به‌عنوان کامپوننت رندر کرد.

جمع‌بندی

  • دستور "use client" در ابتدای فایل (قبل از importها) مرز ورود ماژول‌ها به بسته‌ی مرورگر را تعیین می‌کند و روی فایل‌هایی که از آن import می‌شوند هم اثر می‌گذارد.
  • Client Component هم روی سرور (برای HTML اولیه) و هم در مرورگر (با Hydration) اجرا می‌شود؛ پس دسترسی به window باید داخل رویداد یا Effect باشد.
  • مرز Client را تا حد ممکن کوچک و نزدیک به برگ‌ها نگه دارید و کل صفحه را Client نکنید.
  • Client Component نمی‌تواند async باشد و Propsی که از Server می‌گیرد باید قابل ارسال باشد.
  • در DevNotes اکنون چهار Client Component داریم: NavLink، SearchBox، BackButton و CopyLinkButton.
  • در درس بعد، «State، Event و Hookها»، همین کامپوننت‌ها را عمیق‌تر بررسی می‌کنیم و می‌فهمیم State و رویداد در Client Component چگونه کار می‌کنند.