EduFlow
أساسيات React
⁦0%⁩

الوحدة 2 · الـ Components والـ Props

أنماط التركيب (Composition)

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

الدرس في لمحة

ابنِ components مرنة باستخدام الـ prop المسمّى children وفتحات JSX، وخصّص الـ components العامة، ومرّر العناصر إلى الأسفل لتتجنّب تمرير الـ props عبر طبقات لا تحتاجها.

  1. 1الـ prop المسمّى children
  2. 2أكثر من slot واحد
  3. 3التخصيص
  4. 4التركيب بدلًا من الـ prop drilling

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

سيحتوي Watchlist على عدّة صناديق متشابهة: لوحة «To watch»، ولوحة «Watched»، وربما لوحة «Recommendations» لاحقًا. لكل منها شريط عنوان، وأحيانًا زر أو اثنان على الجانب، ومحتوى مختلف في الداخل. يمكنك إضافة prop لكل نوع محتمل من المحتوى. أو يمكنك أن تدع المستدعي يضع ما يشاء في الداخل، كما تضع أي شيء داخل <div>.

هذا النهج الثاني هو التركيب (composition)، وبه تُبنى أغلب الـ components المرنة في React.

الـ prop المسمّى children

كل ما تضعه بين وسمي component يصل إليه كـ prop اسمه children:

src/components/Panel.jsx
export default function Panel({ title, children }) {
  return (
    <section className="panel">
      <h2>{title}</h2>
      <div className="panel-body">{children}</div>
    </section>
  );
}
src/App.jsx
<Panel title="To watch">
  <MovieList movies={toWatch} />
</Panel>

<Panel title="Watched">
  <p>You've watched {watched.length} movies.</p>
</Panel>

لا يعرف Panel ما في الداخل ولا يهمّه. هو يملك الإطار (الـ section والعنوان والتنسيق)، والمستدعي يملك المحتوى. لقد استخدمت هذا طوال الوقت من دون أن تلاحظ: <main> و<section> يأخذان children أيضًا.

يمكن لـ children أن يكون أي شيء قابل للعرض: عنصرًا واحدًا، أو عدّة عناصر، أو نصًا، أو لا شيء. تعامل معه كصندوق مغلق واعرضه في مكانه. لا تفحصه ولا تعدّه ولا تعدّله؛ وإذا احتاج الغلاف إلى معرفة شيء عن محتواه، فيجب أن تصله تلك المعلومة كـ prop عادي.

تظهر الفائدة حين يتغيّر التصميم. إذا حصلت اللوحات الشهر القادم على زوايا دائرية وظل، فستعدّل Panel مرة واحدة وتتحدّث كل لوحة في التطبيق، مهما كان محتواها.

أكثر من slot واحد

الـ children هو slot (فتحة) واحد. وحين يحتاج الـ component إلى محتوى في عدّة أماكن، استخدم props عادية تحمل JSX. هذه لوحة فيها أزرار اختيارية في ترويستها:

src/components/Panel.jsx
export default function Panel({ title, actions, children }) {
  return (
    <section className="panel">
      <header className="panel-header">
        <h2>{title}</h2>
        {actions}
      </header>
      <div className="panel-body">{children}</div>
    </section>
  );
}
JSX
<Panel
  title="To watch"
  actions={<button onClick={shuffle}>Shuffle</button>}
>
  <MovieList movies={toWatch} />
</Panel>

إذا لم يُمرَّر actions فقيمته undefined، وهي لا تعرض شيئًا، فيصبح الـ slot اختياريًا بلا أي جهد. قارن هذا بواجهة مثل buttons={['Shuffle', 'Clear']}: سيحتاج Panel إلى إنشاء الأزرار، وربط المعالجات، والتعامل مع الأيقونات وحالات التعطيل، وإضافة prop جديد مع كل طلب. أمّا مع الـ slot فيقرّر Panel أين توضع الأشياء، ويقرّر المستدعي ما هي.

التخصيص

أحيانًا تريد نسخة ثابتة من component عام. لا تنسخه؛ بل ارسمه مع props مضبوطة مسبقًا:

src/components/Button.jsx
export function Button({ variant = 'neutral', children, ...rest }) {
  return (
    <button className={`btn btn-${variant}`} {...rest}>
      {children}
    </button>
  );
}

export function DangerButton(props) {
  return <Button variant="danger" {...props} />;
}

الـ component المسمّى DangerButton هو Button اتُّخذ فيه قرار واحد مسبقًا. والـ ...rest في Button يمرّر كل ما عدا ذلك، مثل onClick أو type أو disabled، إلى الـ <button> الحقيقي، فيستخدمه المستدعون كعنصر HTML الذي يعرفونه أصلًا. أصلِح خطأً في Button وستحصل كل الأشكال المشتقّة منه على الإصلاح.

التركيب بدلًا من الـ prop drilling

هذه مشكلة يصادفها كل تطبيق. يملك App الأفلام. ويرسم Layout الـ component المسمّى Sidebar، والشريط الجانبي يعرض WatchCount. لا يحتاج إلى الأفلام إلا WatchCount، لكن لتوصيلها إليه تمرّرها عبر كل مستوى:

JSX
<Layout movies={movies} />           // Layout doesn't use movies…
function Layout({ movies }) {
  return <Sidebar movies={movies} />; // …neither does Sidebar
}

تمرير البيانات عبر components لا تفعل سوى إعادة تمريرها يُسمّى prop drilling (حفر الـ props عبر الطبقات). إنه مملّ، وكل component وسيط صار يعتمد على بيانات لا يستخدمها.

والتركيب يزيله غالبًا. دع App، الذي يملك البيانات، يُنشئ العنصر، ودع Layout يضعه في مكانه:

JSX
export default function App() {
  return (
    <Layout sidebar={<WatchCount movies={movies} />}>
      <MovieList movies={movies} />
    </Layout>
  );
}

function Layout({ sidebar, children }) {
  return (
    <div className="layout">
      <aside>{sidebar}</aside>
      <main>{children}</main>
    </div>
  );
}

لم يعد Layout يعرف أن الأفلام موجودة. صار component تخطيط خالصًا يمكنك إعادة استخدامه في أي صفحة. وتوفّر React أيضًا الـ context للبيانات التي تحتاجها components كثيرة متباعدة، مثل السمة الحالية أو المستخدم المسجّل دخوله. لكن توثيق React يوصي بتجربة التركيب أولًا، وهو ينجح في الغالب.

في الدرس التالي تجعل الـ components تعرض أشياء مختلفة بحسب البيانات، من شارة «Watched» إلى رسالة القائمة الفارغة، باستخدام العرض الشرطي.

أهم النقاط

  • كل ما تضعه بين وسمي الفتح والإغلاق لـ component يصل إليه في الـ prop المسمّى children.
  • يمكن للـ prop أن يحمل JSX، فيستطيع الـ component أن يوفّر عدّة slots مسمّاة مثل actions أو footer.
  • خصّص الـ component العام برسمه مع props مضبوطة مسبقًا بدلًا من نسخه.
  • تمرير عناصر جاهزة إلى الأسفل يغنيك غالبًا عن تمرير البيانات عبر components لا تستخدمها.

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

ما الذي يُعرض داخل <section>؟

JSX
function Panel({ title }) {
  return <section><h2>{title}</h2></section>;
}

<Panel title="To watch">
  <MovieList movies={movies} />
</Panel>
السؤال 2 من 4

تحتاج إلى Panel له عنوان ومحتوى وأزرار اختيارية في الزاوية العليا. أيّ واجهة استخدام هي الأكثر مرونة ووضوحًا؟

السؤال 3 من 4

يملك App المصفوفة movies. ويرسم Layout الـ component المسمّى Sidebar، الذي يرسم WatchCount، وهو الوحيد الذي يحتاج إلى movies. ما الحل بالتركيب (composition) لتجنّب تمرير movies عبر Layout وSidebar؟

السؤال 4 من 4

يأخذ الـ component المسمّى Button الـ prop icon ويعرض <span>{icon}</span>. أيّ استدعاء يعرض أيقونة نجمة؟

تدريب

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

Panel فيه slots

يستخدم App الـ component المسمّى Panel بالطريقة التي يجب أن يعمل بها، لكن Panel يتجاهل معظم ما يُعطى له. عدّل Panel فقط بحيث:

  • يظهر ما يُمرَّر في actions داخل <header>، بعد <h2>؛
  • ويظهر ما يوضع بين وسمي <Panel> (أي children) داخل <section>، بعد <header> لا داخله.

لوحة Watched ليس لها actions، لذا يجب ألّا تعرض أي زر.

function Panel({ title }) {
  // Add an actions slot in the header and render children in the body
  return (
    <section>
      <header>
        <h2>{title}</h2>
      </header>
    </section>
  );
}

export default function App() {
  return (
    <main>
      <Panel title="To watch" actions={<button>Shuffle</button>}>
        <ul>
          <li>Past Lives</li>
          <li>Paddington 2</li>
        </ul>
      </Panel>
      <Panel title="Watched">
        <p>You've watched 1 movie.</p>
      </Panel>
    </main>
  );
}
جارٍ تحميل المحرّر…

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

المعاينة

الـ console

(لا مخرجات)

الفحوص

  • لوحتان بالعنوانين “To watch” و"Watched"
  • تعرض لوحة To watch عنصري القائمة الخاصين بها، خارج الترويسة
  • تعرض لوحة Watched الفقرة الخاصة بها، خارج الترويسة
  • يظهر زر Shuffle داخل ترويسة لوحة To watch
  • لوحة Watched لا تحتوي على أي زر