فرانت اند

کامپوننت‌نویسی اصولی

کامپوننتی که امروز ۳۰ خط است، شش ماه بعد با ده prop و پنج شرط تو‌در‌تو به کابوس تبدیل می‌شود. این مقاله اصولی را مرور می‌کند که کامپوننت‌نویسی را قابل‌نگهداری نگه می‌دارند، با مثال‌هایی از ریکت که در بقیه‌ی فریم‌ورک‌ها هم صدق می‌کنند.

  • نویسنده تیم تتا
  • ۸ دقیقه مطالعه
  • به‌روزرسانی:
کامپوننت‌نویسی اصولی

کامپوننت خوب چه ویژگی‌هایی دارد؟

کامپوننت خوب کوچک، قابل‌پیش‌بینی و قابل‌ترکیب است: با ورودی‌های مشخص (props) خروجی مشخص می‌دهد و از بیرون هم می‌شود فهمید چه کاری می‌کند، بی‌آنکه کدش را بخوانید. معیار عملی ما این است که یک هم‌تیمی بتواند تنها با نام و props آن را درست به کار ببرد.

این اصول مستقل از ابزار هستند، اما مثال‌های ما با ریکت و تایپ‌اسکریپت نوشته شده‌اند، چون تایپ props بهترین مستندات زنده‌ی یک کامپوننت است. اگر هنوز با نگهداری داده در رابط کاربری درگیر هستید، پیش‌تر مدیریت state در ریکت را هم ببینید.

اصل اول: مسئولیت واحد

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

ts
// Presentation only: easy to test and reuse
function ProductCard({ name, price, onAdd }: {
  name: string; price: number; onAdd: () => void;
}) {
  return (
    <article>
      <h3>{name}</h3>
      <p>{price.toLocaleString()}</p>
      <button type="button" onClick={onAdd}>Add</button>
    </article>
  );
}

// Logic lives in a hook, not in the markup
function useProducts(category: string) { /* fetch + cache */ }

منطق دریافت و تبدیل داده را در یک hook یا سرویس بگذارید و کامپوننت نمایشی را تا حد امکان «ساده» نگه دارید. نتیجه، کامپوننتی است که می‌توانید بدون شبکه و در یک صفحه‌ی نمونه (مثل Storybook) ببینید.

اصل دوم: طراحی دقیق props

props رابط عمومی کامپوننت شما هستند و هر prop اضافه، تعهدی برای نگهداری است. چند قاعده که زیاد کمک می‌کنند:

  • به‌جای چند boolean که با هم تداخل دارند، یک union بنویسید: variant: "primary" | "ghost" | "danger" به‌جای isPrimary، isGhost و isDanger.
  • نام رویدادها را با on و نام کنترل‌کننده‌ها را با handle شروع کنید؛ onSelect در رابط، handleSelect در پیاده‌سازی.
  • مقدار پیش‌فرض منطقی بدهید تا ساده‌ترین استفاده کوتاه‌ترین کد باشد.
  • کل یک شیء بزرگ را پاس ندهید؛ فقط همان فیلدهایی که لازم است. این کار وابستگی و رندر اضافه را کم می‌کند.
  • props را با تایپ مشخص کنید و برای دیگران قابل‌کشف بگذارید.
ts
type ButtonProps = {
  variant?: "primary" | "ghost" | "danger";
  size?: "sm" | "md";
  loading?: boolean;
  children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

function Button({ variant = "primary", size = "md", loading, children, ...rest }: ButtonProps) {
  return (
    <button className={"btn btn--" + variant + " btn--" + size}
            disabled={loading || rest.disabled} {...rest}>
      {loading ? "…" : children}
    </button>
  );
}

اصل سوم: ترکیب به‌جای پرچم‌ها و وراثت

وقتی کامپوننتی برای هر نیاز جدید یک prop تازه می‌گیرد (showIcon، hasFooter، footerAlign…) در حال تبدیل‌شدن به یک «کامپوننت خدای» است. راه‌حل این است که قطعه‌های داخلی را بیرون بدهید و به کاربر اجازه دهید آن‌ها را بچیند.

js
// Flag soup
<Card title="Plan" showFooter footerAlign="right" footerText="Buy" />

// Composition
<Card>
  <Card.Header>Plan</Card.Header>
  <Card.Body>…</Card.Body>
  <Card.Footer><Button>Buy</Button></Card.Footer>
</Card>

در ریکت وراثت کلاس تقریباً هرگز لازم نیست؛ ترکیب با children، الگوی compound components و hookهای سفارشی همه‌ی نیازها را پوشش می‌دهند. این رویکرد در مستندات رسمی ریکت هم به‌عنوان روش اصلی توصیه شده است.

تصویر انتزاعی و رنگی از قطعات لگو‌مانند هندسی که به شکل یک رابط کاربری کنار هم چیده شده‌اند
کامپوننت‌های کوچک و قابل‌ترکیب را می‌توان چند بار و به چند شکل دوباره استفاده کرد؛ کامپوننت غول‌پیکر فقط یک بار.

اصل چهارم: state را نزدیک نگه دارید

state را در پایین‌ترین کامپوننتی تعریف کنید که همه‌ی مصرف‌کننده‌هایش زیر آن هستند. بالا بردن بی‌دلیل state باعث رندر دوباره‌ی شاخه‌های بزرگ و prop drilling می‌شود. دو مرحله‌ی تصمیم‌گیری ساده:

  1. اگر فقط یک کامپوننت به آن نیاز دارد، همان‌جا نگهش دارید (مثلاً باز یا بسته بودن یک منو).
  2. اگر چند خواهر‌وبرادر نیاز دارند، به نزدیک‌ترین والد مشترک ببرید؛ اگر عمق زیاد شد، Context یا یک کتابخانه‌ی مدیریت state.

اصل پنجم تا هفتم: دسترس‌پذیری، تست و پایداری

دسترس‌پذیری از اولین نسخه

دکمه‌ی سفارشی که با div ساخته شود، در همه‌جا تکرار می‌شود و اصلاحش هم باید در همه‌جا تکرار شود. برای همین، کامپوننت‌های پایه (دکمه، فیلد، مودال) باید از ابتدا HTML معنادار، برچسب و مدیریت فوکوس داشته باشند؛ موارد کامل در چک‌لیست دسترس‌پذیری وب آمده است.

تست‌پذیری

کامپوننتی که از بیرون به رفتار قابل‌مشاهده‌اش (متن، نقش، رویداد) تست شود، با بازسازی داخلی نمی‌شکند. از Testing Library و پرسش بر اساس نقش و متن استفاده کنید، نه بر اساس نام کلاس CSS.

پایداری و عملکرد

پیش از استفاده از React.memo و useMemo، اندازه بگیرید. بیشتر مشکل‌های سرعت از رندر بی‌دلیل شاخه‌های بزرگ یا تصاویر سنگین می‌آیند، نه از خود کامپوننت؛ برای بررسی کلی، بهینه‌سازی سرعت بارگذاری صفحه را ببینید. اگر می‌خواهید یک سیستم طراحی و کتابخانه‌ی کامپوننت برای محصولتان داشته باشید، تیم توسعه‌ی وب ما آماده‌ی همکاری است.

پرسش‌های متداول

یک کامپوننت چقدر بزرگ باید باشد؟

قاعده‌ی عددی دقیقی وجود ندارد، اما اگر فایل از حدود ۱۵۰ خط بیشتر شد یا مسئولیت‌هایش را نمی‌توانید در یک جمله بگویید، معمولاً وقت شکستن آن است.

تفاوت کامپوننت کنترل‌شده و کنترل‌نشده چیست؟

در کامپوننت کنترل‌شده، مقدار از props می‌آید و والد آن را مدیریت می‌کند؛ در کنترل‌نشده، کامپوننت state داخلی خودش را دارد. کتابخانه‌های خوب هر دو حالت را پشتیبانی می‌کنند.

چه زمانی باید از Context استفاده کنم؟

برای داده‌ای که به‌ندرت تغییر می‌کند و در عمق زیادی نیاز است، مثل تم، زبان یا کاربر فعلی. برای داده‌ای که مدام به‌روز می‌شود، Context رندرهای زیادی ایجاد می‌کند.

آیا باید برای هر چیز کوچکی کامپوننت بسازم؟

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

دیدگاه‌ها

    هنوز دیدگاهی ثبت نشده است. اولین نفر باشید.

    دیدگاه‌ها پس از بررسی منتشر می‌شوند.