كن مطوّر واجهات أمامية محترفا
10 دقيقة قراءة

مكوّنات React وخصائصها: التركيب من الصفر إلى الاحتراف

دليل عميق وعملي لتصميم مكوّنات React وتنميط خصائصها بـ TypeScript — الخصائص كعقدٍ للقراءة فقط، وتنميطها بـ type مقابل interface، والقيم الاختيارية والافتراضية، وchildren وReact.ReactNode، وأنماط التركيب (الأغلفة والفتحات والمكوّنات المتخصّصة)، وتمرير الخصائص إلى DOM عبر rest وReact.ComponentProps، وخصائص الأنماط باتحادات مُميَّزة، وتنقيط الخصائص ولماذا يتفوّق التركيب على الضبط، والأخطاء الشائعة، مع تمارين عملية وحلولها.

بمجرّد أن تعرف أن المكوّن مجرّد دالّة تُرجِع واجهة، تصير المهارة الحقيقية في تصميم تلك المكوّنات: ما الخصائص التي تأخذها، وكيف تتركّب، وكيف تُنمِّطها بحيث يفشل سوء الاستخدام في الترجمة بدل وقت التشغيل. هذه التدوينة عن تلك الحرفة — الفرق بين مكتبة مكوّناتٍ تُسعِد مستخدمها وأخرى تُقاومه. تبني مباشرةً على أساسيات React وTypeScript التي تعرفها؛ ونكتب كل شيءٍ بـ .tsx.

المبدأ الوحيد الذي يشكّل كل واجهة مكوّناتٍ جيّدة: خصائص المكوّن عقد. تُصرّح بالضبط بما يحتاجه المكوّن، وتتدفّق باتجاهٍ واحد (نزولًا، للقراءة فقط)، ومع TypeScript تجعل مُدقّق الأنواع يفرض الاستخدام الصحيح. صمّم العقد جيّدًا فيصير المكوّن بديهيّ الاستخدام؛ صمّمه رديئًا فيصير كل موضع استدعاءٍ لعبة تخمين.

الخصائص عقدٌ للقراءة فقط

الخصائص هي مُدخَلات المكوّن — نفس فكرة معاملات الدالّة. الأب يزوّدها؛ والابن يقرؤها ويجب ألّا يغيّرها أبدًا. هذا التدفّق أحاديّ الاتجاه للقراءة فقط هو ما يُبقي تطبيق React قابلًا للتتبّع: أيّ قيمةٍ على الشاشة جاءت من خاصّةٍ مرّرها أبٌ ما فوقها.

type PriceProps = { amount: number; currency: string };

function Price({ amount, currency }: PriceProps) {
  // amount = amount * 1.2;  // ❌ لا تغيّر الخصائص أبدًا — إنها ملك الأب
  const withTax = amount * 1.2;   // ✅ اشتقّ قيمةً محلّية جديدة بدلًا من ذلك
  return <span>{withTax.toFixed(2)} {currency}</span>;
}

وإن احتاج الابن تغيير شيء، فلا يلمس الخاصّة — بل يمرّر الأب نزولًا ردّ نداءٍ يستدعيه الابن. البيانات تتدفّق نزولًا؛ والأحداث تتدفّق صعودًا. (نمط ردّ النداء هذا هو كيف ترفع الحالة، وهو مشروح في تدوينة الحالة والخطّافات.)

تنميط الخصائص: type مقابل interface

صِف خصائص المكوّن بمرادف type أو بـ interface. لخصائص المكوّنات، يكون type الخيار الشائع الافتراضيّ — فهو يتعامل مع الاتحادات والتقاطعات التي لا يعبّر عنها interface بالنظافة نفسها. استخدم interface حين تريد شكلًا عامًّا قابلًا للتوسيع (مثل خاصّةٍ في نظام تصميمٍ يوسّعها المستهلكون).

// type — الخيار اليوميّ
type AvatarProps = {
  src: string;
  alt: string;
  size?: number;        // اختيارية — العلامة ? تعني أن المستدعي قد يحذفها
  rounded?: boolean;
};

function Avatar({ src, alt, size = 40, rounded = true }: AvatarProps) {
  return (
    <img
      src={src}
      alt={alt}
      width={size}
      height={size}
      className={rounded ? "avatar avatar--round" : "avatar"}
    />
  );
}

عادتان تجعلان الخصائص متينة:

  • علّم الخصائص الاختيارية حقًّا بـ ? وأعطِها قيمةً افتراضية في التفكيك (size = 40). النوع يقول "لك أن تحذفها"؛ والقيمة الافتراضية تقول "هذا ما يحدث إن فعلت".
  • كن دقيقًا في الأنواع. فضّل اتحاد حرفيّاتٍ ("sm" | "md" | "lg") على string مجرّدة، وشكلًا محدّدًا على object. كلما ضاق النوع، التقط المترجم أخطاءً أكثر.
type ButtonProps = {
  label: string;
  size?: "sm" | "md" | "lg";   // ✅ هذه الثلاثة فقط مسموحة
  onClick: () => void;
};

children وReact.ReactNode

أيّ شيءٍ تعشّقه بين وسمَي المكوّن يصل كخاصّة children. نمِّطها بـ React.ReactNode — الجامع لكل شيءٍ قابل للعرض (عناصر، نصوص، أعداد، مصفوفات، null). هذا أساس مكوّنات الأغلفة:

type CardProps = {
  title: string;
  children: React.ReactNode;   // أيًّا كان ما يعشّقه المستدعي بالداخل
};

function Card({ title, children }: CardProps) {
  return (
    <article className="card">
      <h3 className="card__title">{title}</h3>
      <div className="card__body">{children}</div>
    </article>
  );
}

// المستدعي يقرّر ما بالداخل — والـ Card لا يهتمّ
<Card title="Invoice">
  <p>Amount due: $240</p>
  <button>Pay now</button>
</Card>

children هو ما يجعل المكوّن وعاءً قابلًا لإعادة الاستخدام لا قالبًا ثابتًا.

أنماط التركيب

التركيب — تجميع مكوّناتٍ صغيرة في أكبر — هو آلية إعادة الاستخدام الجوهرية في React. وهذه الأنماط ستطلبها باستمرار، من الأبسط إلى الأكثر مرونة.

1. مكوّنات الأغلفة

مكوّنٌ مهمّته تغليف children ببنيةٍ أو تنسيقٍ متّسق — Card وPanel وSection وModal. الذي في الأعلى هو النموذج الأصليّ. يعرف كيف يعرض، لا ماذا يعرض.

2. الفتحات: تمرير JSX كخصائص

children يمنحك فجوةً واحدة تملؤها. وحين يملك المكوّن عدّة مناطق متمايزة — ترويسة، وشريط جانبيّ، وتذييل — أعطِ كلًّا خاصّته المُنمَّطة بـ React.ReactNode. تُسمّى هذه غالبًا فتحات (slots):

type LayoutProps = {
  header: React.ReactNode;
  sidebar: React.ReactNode;
  children: React.ReactNode;   // فتحة المحتوى الرئيسيّ
};

function Layout({ header, sidebar, children }: LayoutProps) {
  return (
    <div className="layout">
      <header className="layout__header">{header}</header>
      <aside className="layout__sidebar">{sidebar}</aside>
      <main className="layout__main">{children}</main>
    </div>
  );
}

<Layout header={<Logo />} sidebar={<Nav />}>
  <Dashboard />
</Layout>

يملأ الأب كل فتحة، فيبقى Layout مجرّد ترتيبٍ نقيّ للمناطق بلا أيّ معرفةٍ بما يملؤها.

3. المكوّنات المتخصّصة

بدل مكوّنٍ واحدٍ بعشرات الرايات، ابنِ عامًّا واحدًا ولُفّه في تخصّصاتٍ مُسمّاة. يضبط التخصّص القيم المشتركة الافتراضية فتبقى مواضع الاستدعاء نظيفة:

function Button({ variant = "secondary", ...props }: ButtonProps) {
  return <button className={`btn btn--${variant}`} {...props} />;
}

// أغلفة متخصّصة — القصد واضح عند موضع الاستدعاء
const PrimaryButton = (props: Omit<ButtonProps, "variant">) => (
  <Button variant="primary" {...props} />
);
const DangerButton = (props: Omit<ButtonProps, "variant">) => (
  <Button variant="danger" {...props} />
);

Omit<ButtonProps, "variant"> أداة TypeScript تعيد استخدام ButtonProps لكن تزيل variant — الغلاف يتحكّم فيه، فلا يستطيع المستدعون تمريره (ولا يحتاجونه).

تمرير الخصائص إلى DOM

Button أو Input قابلٌ لإعادة الاستخدام ينبغي أن يقبل كل سمات HTML العادية — onClick، disabled، aria-label، type — دون أن تعيد التصريح بكلٍّ منها. قطعتان تجعلان هذا نظيفًا: React.ComponentProps لوراثة أنواع خصائص عنصر، ومُعامل rest/spread لتمريرها.

// ورِث كل خاصّة <button> أصلية، ثم أضِف خصائصك
type ButtonProps = React.ComponentProps<"button"> & {
  variant?: "primary" | "secondary" | "danger";
};

function Button({ variant = "secondary", className, ...rest }: ButtonProps) {
  return (
    <button
      className={`btn btn--${variant} ${className ?? ""}`}
      {...rest}   // يمرّر onClick وdisabled وtype وaria-* وكل ما تبقّى
    />
  );
}

// يحصل المستدعون على وصولٍ كامل مُدقَّق النوع للسمات الأصلية
<Button variant="primary" onClick={save} disabled={busy} type="submit">
  Save
</Button>

هذا هو النمط الاحترافيّ لأوّليّات أنظمة التصميم: اسحب الخصائص التي تعالجها خصّيصًا (variant، className)، وانشُر الباقي على العنصر الأساسيّ. يحصل المستدعون على مكوّنٍ يتصرّف تمامًا كالعنصر الأصليّ، إضافةً إلى تحسيناتك — مُنمَّطًا بالكامل.

خصائص الأنماط باتحاداتٍ مُميَّزة

أحيانًا تعتمد الخصائص المسموحة للمكوّن على خاصّةٍ أخرى. حالةٌ كلاسيكية: زرٌّ إمّا <button> حقيقيّ (يحتاج onClick) أو رابط (يحتاج href). نمذجة هذا بخصائص كلها اختيارية تتيح للمستدعين تمرير هراء (href وonClick معًا، أو لا شيء منهما). الاتحاد المُميَّز يجعل التركيبات غير الصالحة مستحيلة الكتابة:

type ActionProps =
  | { as: "button"; onClick: () => void; href?: never }
  | { as: "link"; href: string; onClick?: never };

function Action(props: ActionProps) {
  if (props.as === "link") {
    return <a href={props.href}>{/* ... */}</a>;   // TS تعرف أن href موجودة هنا
  }
  return <button onClick={props.onClick}>{/* ... */}</button>;
}

<Action as="button" onClick={save} />   // ✅
<Action as="link" href="/home" />        // ✅
<Action as="link" onClick={save} />      // ❌ خطأ ترجمة: الرابط بلا onClick

الحقل as هو المُميِّز — فحصه يضيّق النوع فتعرف TypeScript بالضبط أيّ خصائص متاحة في كل فرع. هكذا تُرمِّز "هذه الخصائص تتلازم، وتلك لا" في نظام الأنواع بدل if وقت التشغيل.

تنقيط الخصائص — ولماذا يتفوّق التركيب على الضبط

تنقيط الخصائص (prop drilling) هو تمرير خاصّةٍ نزولًا عبر عدّة طبقات مكوّناتٍ لا تستخدمها هي، لمجرّد بلوغ ابنٍ عميق. القليل منه مقبول؛ والكثير رائحةٌ كريهة.

// تنقيط: user يمرّ عبر Page وHeader لمجرّد بلوغ Avatar
<Page user={user}>          // Page لا تستخدم user...
  <Header user={user}>      // ...ولا Header...
    <Avatar user={user} />  // ...وحده Avatar يحتاجه فعلًا
  </Header>
</Page>

مخرجان، بترتيب الأفضلية:

  1. التركيب — مرّر العنصر الجاهز كخاصّةٍ أو ابن بدل حياكة بياناته عبر الطبقات. دع المستوى الأعلى يبني Avatar ويسلّمه نزولًا كمحتوى:
// الطبقة التي تملك user تبني Avatar؛ والطبقات الوسطى تعرض children فقط
<Page>
  <Header avatar={<Avatar user={user} />} />
</Page>

الآن لا يذكر Page ولا Header كلمة user — يضعان ما يُعطيان فقط. هذا وحده يذيب معظم التنقيط.

  1. السياق (Context) — للبيانات العالمية حقًّا (السمة، المستخدم الحاليّ، اللغة) التي تحتاجها مكوّناتٌ كثيرة متناثرة، يتيح سياق React لمزوِّدٍ أن يبثّ قيمةً يقرؤها أيّ سليلٍ مباشرةً. (موضوع خطّافات — مشروح في تدوينة الحالة والخطّافات.) اطلبه فقط حين يكون التركيب أخرق، لأن السياق يقايض الوضوح بالراحة.

الدرس الأوسع: حين ينمو مكوّنٌ بغابةٍ من الرايات المنطقية (isCompact، hasIcon، showFooter، variant...)، فذلك ضبطٌ ينوء تحت ثقله. التركيب — مكوّناتٌ أصغر تُدمَج عبر children والفتحات — يعبّر عادةً عن التنوّع نفسه بأوضح، ويبقى منفتحًا على تركيباتٍ لم تتوقّعها.

الأخطاء الشائعة

  • تغيير الخصائصprops.items.push(x) أو props.user.name = .... الخصائص ملك الأب؛ اشتقّ قيمًا محلّية جديدة أو استدعِ ردّ نداء.
  • انفجار الرايات المنطقية — مكوّنٌ بعشر خصائص is*/has* هو غالبًا عدّة مكوّناتٍ في معطف. قسّمه وركّب.
  • تنميط الخصائص بـ any أو object — تخسر كل ضمان. صِف الشكل الحقيقيّ؛ واستخدم اتحادات الحرفيّات للخيارات المحدودة.
  • إعادة التصريح بالسمات الأصلية يدويًّا — كتابة onClick وdisabled وغيرها. ورِثها بـ React.ComponentProps<"button"> وانشُر ...rest.
  • نسيان تمرير className/...rest — غلافٌ يبتلع الخصائص الإضافية لا يستطيع مستدعوه تنسيقه أو توسيعه. ادمج className وانشُر الباقي.
  • خصائص كلها اختيارية لحالاتٍ متنافية — تتيح للمستدعين تمرير تركيباتٍ غير صالحة. نمذجها باتحادٍ مُميَّز.
  • تنقيطٌ عميق للخصائص — حياكة قيمةٍ عبر طبقاتٍ لا تهتمّ بها. سلّم العنصر الجاهز نزولًا (تركيب)، أو استخدم السياق للبيانات العالمية حقًّا.
  • تعريف مكوّناتٍ داخل مكوّناتٍ أخرى — دالّة function Child() مُعرّفة في جسم الأب هي مكوّنٌ جديد في كل عرض، يعيد تركيب شجرته الفرعية. عرّف المكوّنات في أعلى مستوى الوحدة.

تمارين

جرّب كلًّا قبل فتح الحلّ.

تمرين 1 — نمِّط عقد خصائص

اكتب type لمكوّن Toast يأخذ message نصًّا مطلوبًا، وtone اختياريًّا لا يكون إلا "info" أو "success" أو "error"، وردّ نداء onDismiss اختياريًّا.

اعرض الحل
type ToastProps = {
  message: string;
  tone?: "info" | "success" | "error";
  onDismiss?: () => void;
};

يقيّد اتحاد الحرفيّات tone بثلاث قيمٍ بالضبط — أيّ غيرها خطأ ترجمة. وتستخدم الخاصّتان الاختياريتان ?، ويُنمَّط ردّ النداء كدالّةٍ لا تُرجِع شيئًا.

تمرين 2 — غلافٌ بـ children

ابنِ مكوّن Field يعرض <label> (من خاصّة label) فوق أيّ حقلٍ تعشّقه بداخله.

اعرض الحل
type FieldProps = { label: string; children: React.ReactNode };

function Field({ label, children }: FieldProps) {
  return (
    <label className="field">
      <span className="field__label">{label}</span>
      {children}
    </label>
  );
}

<Field label="Email">
  <input type="email" />
</Field>

children يتيح لـ Field تغليف أيّ عنصر تحكّم — input أو select أو textarea — دون معرفة أيّها.

تمرين 3 — مرّر الخصائص الأصلية

اصنع Input يقبل كل سمات <input> الأصلية إضافةً إلى invalid منطقيّ اختياريّ يضيف صنف خطأ.

اعرض الحل
type InputProps = React.ComponentProps<"input"> & { invalid?: boolean };

function Input({ invalid = false, className, ...rest }: InputProps) {
  return (
    <input
      className={`input ${invalid ? "input--error" : ""} ${className ?? ""}`}
      {...rest}
    />
  );
}

React.ComponentProps<"input"> يرث كل سمةٍ أصلية؛ تسحب ما تعالجه (invalid، className) وتنشُر الباقي على العنصر.

تمرين 4 — اقتل التنقيط

user مُنقَّط عبر PageToolbarUserMenu، لكن وحده UserMenu يستخدمه. أعِد الهيكلة بالتركيب كي لا تذكر الطبقات الوسطى user.

اعرض الحل
// Toolbar يأخذ فتحةً بدل البيانات الخام
type ToolbarProps = { menu: React.ReactNode };
function Toolbar({ menu }: ToolbarProps) {
  return <div className="toolbar">{menu}</div>;
}

// المستوى الذي يملك user يبني UserMenu ويمرّره نزولًا كعنصر
<Page>
  <Toolbar menu={<UserMenu user={user} />} />
</Page>

يضع Toolbar الآن أيّ عنصرٍ يُسلَّم إليه فقط، فلا يمرّ user عبر طبقةٍ لا تحتاجه.

النموذج الذهني الذي تحتفظ به

خصائص المكوّن عقدٌ للقراءة فقط: الأب يزوّدها، والابن يقرؤها ولا يغيّرها أبدًا، والبيانات تتدفّق نزولًا بينما تتدفّق الأحداث صعودًا عبر ردود النداء. نمِّط ذلك العقد بدقّةٍ بـ TypeScript — مرادفات type، و? للخصائص الاختيارية مقرونةً بـقيمٍ افتراضية، واتحادات حرفيّات بدل نصوصٍ مجرّدة — كي يفشل الاستخدام الخطأ في الترجمة. املأ المكوّنات بالمحتوى عبر children (المُنمَّطة React.ReactNode) وعدّة فتحات للتخطيطات متعدّدة المناطق، وأعِد استخدام السلوك بـتركيب قطعٍ صغيرة بدل تكديس الرايات على مكوّنٍ عملاق. ولأوّليّات أنظمة التصميم، ورِث السمات الأصلية بـ**React.ComponentProps** ومرّرها بـ**...rest** كي يتصرّف Button كزرٍّ حقيقيّ؛ ورمّز "هذه الخصائص تتلازم" بـاتحاداتٍ مُميَّزة. وحين يجب أن تبلغ قيمةٌ ابنًا عميقًا، فضّل التركيب (سلّم العنصر الجاهز نزولًا) على تنقيط الخصائص، واحفظ السياق للبيانات العالمية حقًّا. أتقِن عقد الخصائص والتركيب، فتستطيع تصميم واجهات مكوّناتٍ بديهيّة الاستخدام ومستحيلة الإساءة — وتلك هي العلامة الحقيقية لاحتراف React.