EduFlow

الوحدة 3 · المكوّنات

الـ Component API: الـ Props والـ Variants والـ Slots

22 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

صمّم component API يستخدم variants من نوع enum بدل الأعلام المنطقية، ومفردات props موحّدة، وتمريرًا للسمات الأصلية، وslots للمحتوى، مع Button الخاص بـ Northwind مثالًا.

  1. 1الـ enums للاختيارات، والقيم المنطقية للحالات
  2. 2مفردات واحدة عبر النظام
  3. 3لا تحجب المنصّة
  4. 4الـ slots للمحتوى
  5. 5الـ variants في CSS: حوّل الـ props إلى سمات
  6. 6راجع الـ API قبل أن تبنيه

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

كان لـ Button الذي أطلقته Northwind عام 2021 تسعة props: primary، secondary، danger، small، large، outline، block، icon، وonPress. والقيم المنطقية (boolean) السبع منها وحدها تسمح بـ 128 تركيبة، ووجدت الفرق معظمها. كان في التطبيق الإنتاجي <Button primary danger small large>. كان يعرض شيئًا ما، ولم يستطع أحد أن يقول ماذا كان يُفترض أن يكون. الـ component API عقدٌ مع كل فريق يستخدم المكوّن، وكأي عقد، الجزء الصعب هو ما يسمح به.

الـ enums للاختيارات، والقيم المنطقية للحالات

القاعدة الأولى تخصّ شكل الـ props. إن كانت الخيارات متنافية، فمكانها prop واحد من نوع enum. وإن كان الشيء يعمل أو لا يعمل، فهو قيمة منطقية (boolean).

Button.tsx
type ButtonProps = React.ComponentProps<'button'> & {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
  startIcon?: React.ReactNode;
  endIcon?: React.ReactNode;
};

حين يكون variant من نوع enum، لا يمكن أن يكون primary وdanger صحيحين معًا؛ ونظام الأنواع يرفض ذلك قبل مراجعة الكود. أما loading فيبقى قيمة منطقية، لأن الزر إما في حالة تحميل أو لا، بشكل مستقل عن الـ variant الخاص به.

مفردات واحدة عبر النظام

القاعدة الثانية هي الاتّساق بين المكوّنات. إن استخدم Button القيمة size="sm"، فكذلك يفعل Input وSelect وTabs وBadge، بالقيم الثلاث نفسها. وإن سمّى أحد المكوّنات معالج التغيير onChange ومرّر إليه القيمة الجديدة، فكلها تفعل ذلك. تحتفظ Northwind بمسرد API من صفحة واحدة لأسماء الـ props المشتركة، والمكوّنات الجديدة تعيد استخدام هذه الأسماء ما لم يوجد سبب قوي للخروج عنها.

وهذا أهمّ من أي قرار منفرد بشأن prop. المهندس الذي استخدم ثلاثة مكوّنات من Northwind يجب أن يستطيع تخمين API المكوّن الرابع بشكل صحيح. وكل تناقض ضريبة صغيرة يدفعها الجميع، كل يوم.

لا تحجب المنصّة

القاعدة الثالثة: يجب ألا يسلب المكوّن أبدًا ما يقدّمه HTML أصلًا. الـ Button هو <button>، لذا يجب أن يقبل كل ما يقبله <button>، بما في ذلك سمات aria-*، وأن يمرّره:

Button.tsx
export function Button({
  variant = 'secondary',
  size = 'md',
  loading = false,
  startIcon,
  endIcon,
  type = 'button',
  children,
  ...rest
}: ButtonProps) {
  return (
    <button
      {...rest}
      type={type}
      className={['nw-button', rest.className].filter(Boolean).join(' ')}
      data-variant={variant}
      data-size={size}
      aria-busy={loading || undefined}
    >
      {startIcon}
      <span className="nw-button__label">{children}</span>
      {endIcon}
    </button>
  );
}

لأن ref صار prop عاديًا في مكوّنات الدوال في React 19، فإنه يمرّ عبر ...rest مع كل شيء آخر، فتعمل إدارة التركيز (focus) والـ tooltips دون تغليف إضافي. وللسبب نفسه تعمل onClick وdisabled وform وaria-describedby.

الـ slots للمحتوى

الـ slot (الفتحة) مكان يمرّر فيه المستخدم محتوى بدل الإعدادات. children هو الـ slot الرئيسي؛ وstartIcon وendIcon هما slots مسمّاة. قارن بين طريقتين لإضافة أيقونة:

TSX
<Button icon="truck" iconPosition="left">Assign</Button>        // configuration
<Button startIcon={<TruckIcon />}>Assign</Button>               // slot

نسخة الإعدادات تحتاج أن يعرف الـ Button كل اسم أيقونة وكل موضع، ويُضاف إليها prop كلما أراد أحد شيئًا جديدًا. أما نسخة الـ slot فتقبل أي عنصر، فتستطيع الفرق تمرير مجموعة أيقونات مختلفة أو صورة رمزية صغيرة دون إصدار جديد. والدرس التالي يأخذ هذه الفكرة أبعد بكثير.

الـ variants في CSS: حوّل الـ props إلى سمات

يكتب الـ Button قيم الـ props الخاصة به في السمتين data-variant وdata-size، فيعكس الـ CSS الـ API واحدًا لواحد. وكل variant يغيّر الـ component tokens وحدها:

CSS
.nw-button[data-variant="primary"] {
  --nw-button-bg: var(--nw-color-action-bg);
  --nw-button-text: var(--nw-color-action-text);
  --nw-button-border: transparent;
}

الـ data attributes تُبقي نقاط ربط الأنماط مقروءة في DevTools، حيث ترى data-variant="primary" بدل اسم class مولَّد بالـ hash لا يُقرأ، ولا يمكن جمعها في تركيبة عبثية كما يمكن مع classes مثل .primary.danger.

راجع الـ API قبل أن تبنيه

تكتب Northwind كود الاستخدام قبل كود المكوّن. كل اقتراح مكوّن جديد يبدأ بثلاثة مقاطع: الاستخدام الأكثر شيوعًا، والاستخدام الأعقد الذي طلبه أحد، واستخدام واحد يجب أن يكون مستحيلًا. ثم تطرح المراجعة أربعة أسئلة. هل تطابق أسماء الـ props مسرد الـ API؟ هل يمكن لأي تركيبة من الـ props أن تصف شيئًا بلا معنى؟ هل يمرّر المكوّن كل ما يدعمه عنصره الأصلي؟ وما الذي سيحتاجه الفريقان الثاني والثالث اللذان سيستخدمانه ولم يحتجه الفريق الأول؟

تستغرق هذه المراجعة نحو ثلاثين دقيقة. أما تغيير الـ API بعد أن يعتمد عليه عشرون فريقًا فيتطلّب إصدارًا رئيسيًا (major)، ودليل انتقال، وشهورًا من المتابعة، وهذا موضوع القسم 4.

في التمرين تكتب قواعد الـ variant والحجم لهذا الـ Button باستخدام الـ component tokens وحدها. وبعدها ترى ما يحدث حين يتجاوز المكوّن حدود الـ props كليًا.

أهم النقاط

  • استخدم prop واحدًا من نوع enum مثل variant للخيارات المتنافية؛ فالقيم المنطقية المنفصلة تسمح بتركيبات لا معنى لها.
  • اجعل أسماء الـ props وقيمها متطابقة عبر المكوّنات، حتى يعني size="sm" الشيء نفسه على Button وInput وSelect.
  • مرّر السمات الأصلية والـ ref إلى العنصر الأساسي، حتى لا يحجب المكوّن أبدًا ما يقدّمه HTML أصلًا.
  • القيم الافتراضية قرارات تصميم؛ اختر الخيار الآمن والشائع، مثل type="button" والـ variant الثانوي.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

يقبل Button القديم في Northwind القيم المنطقية primary وdanger وoutline. كتب أحد الفرق <Button primary danger>. ما مشكلة الـ API الأساسية؟

السؤال 2 من 3

زرّ Button داخل <form> يواصل إرسال النموذج حين يضغط الناس «Add stop». الـ Button يعرض <button> بلا type. ما أفضل إصلاح داخل نظام التصميم؟

السؤال 3 من 3

أيّ تصميم للـ props هو الأكثر اتّساقًا بين مكوّنات النظام؟

تدريب

بيئة تجريب الكود

نسّق الـ variants الخاصة بالـ Button بالـ component tokens

يعرض الـ Button قيم الـ props الخاصة به كسمتين data-variant وdata-size. والقاعدة الأساسية .nw-button هي الـ variant الثانوي. أضف ثلاث قواعد لا تحدّد إلا component tokens من نوع --nw-button-… (بلا تعريفات background أو color أو padding):

  • data-variant="primary": خلفية الإجراء ونصّه من الـ semantic tokens، وحدّ شفاف.
  • data-variant="danger": خلفية الخطر ونصّه من الـ semantic tokens، وحدّ شفاف.
  • data-size="sm": حشوة 4px عموديًا و12px أفقيًا.

يجب أن يكون الزر الأساسي الصغير صغيرًا وأساسيًا معًا.

<div class="row">
  <button class="nw-button" type="button" id="b-default">Cancel</button>
  <button class="nw-button" type="button" data-variant="primary" id="b-primary">Assign driver</button>
  <button class="nw-button" type="button" data-variant="danger" id="b-danger">Delete route</button>
  <button class="nw-button" type="button" data-variant="primary" data-size="sm" id="b-small">Add stop</button>
</div>جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

المعاينة

الـ console

(لا مخرجات)

الفحوص

  • الزر بلا variant يبقى ثانويًا (أبيض بحدّ رمادي)
  • data-variant="primary" يستخدم ألوان الإجراء مع حدّ شفاف
  • data-variant="danger" يستخدم ألوان الخطر مع حدّ شفاف
  • data-size="sm" يقلّص الحشوة ويتركّب مع primary
  • قواعد الـ variant والحجم لا تحدّد إلا tokens من نوع --nw-button-…