اذهب إلى المحتوى

تقسيم تطبيق React إلى مكونات


Ola Abbas

يُعَدّ تطبيقنا الذي عملنا عليه في المقال السابق وحدةً متراصةً، لذلك يجب تقسيمه إلى مكوّنات يمكن وصفها وإدارتها قبل تمكننا من جعل تطبيقنا يفعل شيئًا ما، إذ لا تحتوي مكتبة React على قواعد صارمة لتحديد ما يُعَدّ مكوّنًا Component، فالأمر متروك لك، وسنعرض في هذا المقال طريقةً معقولةً لتقسيم تطبيقنا إلى مكونات.

تحديد المكون الأول

قد يبدو تحديد أحد المكوّنات أمرًا صعبًا إلى حين حصولك على بعض الخبرة العملية، ولكن الأمور المهمة هي:

  • إذا كان أحد الأشياء جزءًا واضحًا من تطبيقك، فيُحتمَل أن يكون مكوّنًا.
  • إذا أُعيد استخدام أحد الأشياء كثيرًا، فيُحتمَل أن يكون مكوّنًا.

تُعَدّ النقطة الثانية ذات قيمة خاصة، إذ يتيح إنشاء مكوّن من عناصر واجهة المستخدِم تعديلَ شيفرتك البرمجية في مكان واحد ورؤية تلك التعديلات في جميع الأماكن التي يُستخدَم فيها هذا المكوّن، إذ ليس عليك تقسيم كل شيء إلى مكوِنات مباشرةً، ولنستخدِم النقطة الثانية لإنشاء مكوّن من أكثر الأجزاء المُعاد استخدامها والأكثر أهميةً في واجهة المستخدِم وهو عنصر قائمة المهام.

إنشاء المكون <Todo /‎>

يجب علينا إنشاء ملف جديد للمكوّن قبل إنشائه، ويجب إنشاء مجلد لهذه المكونات، إذ تنشئ الأوامر التالية المجلد components وملفًا ضمنه يُسمَّى Todo.js، ولكن تأكّد من وجودك في جذر تطبيقك قبل تشغيل هذه الأوامر:

mkdir src/components
touch src/components/Todo.js

إنّ ملف Todo.js الجديد فارغ حاليًا، لذا افتحه واكتب فيه السطر الأول التالي:

import React from "react";

سننشئ مكوّنًا يسمّى Todo، لذلك يمكننا إضافة شيفرتنا إلى الملف Todo.js على النحو التالي، إذ سنعرِّف الدالة Todo()‎ ونصدّرها على السطر نفسه كما يلي:

export default function Todo() {
  return (

  );
}

كل شيء جيد حتى الآن، لكن يجب أن يعيد المكون شيئًا ما، لذا ارجع إلى الملف src/App.js، وانسخ أول عنصر <li> من القائمة غير المرتبة، والصقه في الملف Todo.js بحيث يصبح كما يلي:

export default function Todo() {
  return (
    <li className="todo stack-small">
      <div className="c-cb">
        <input id="todo-0" type="checkbox" defaultChecked={true} />
        <label className="todo-label" htmlFor="todo-0">
          Eat
        </label>
      </div>
      <div className="btn-group">
        <button type="button" className="btn">
          Edit <span className="visually-hidden">Eat</span>
        </button>
        <button type="button" className="btn btn__danger">
          Delete <span className="visually-hidden">Eat</span>
        </button>
      </div>
    </li>
  );
}

ملاحظة: يجب أن تعيد المكونات شيئًا ما دائمًا، فإذا حاولت لاحقًا تصيير Render مكون لا يعيد شيئًا، فستعرِض React خطأً في متصفحك.

أصبح المكوّن Todo مكتملًا حاليًا، وبالتالي يمكننا استخدامه، والآن أضف السطر التالي في الملف App.js بالقرب من أعلى الملف لاستيراد المكوّن Todo:

import Todo from "./components/Todo";

يمكنك مع استيراد هذا المكون وضع استدعاءات المكوّن <Todo /‎> مكان جميع عناصر <li> في الملف App.js، ويجب أن يصبح العنصر <ul> كما يلي:

<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading"
>
  <Todo />
  <Todo />
  <Todo />
</ul>

إذا نظرت إلى متصفحك، فستلاحظ شيئًا مؤسفًا، إذ تُكرِّر قائمتك المهمة الأولى ثلاث مرات كما يلي:

01_todo-list-repeating-todos.png

لا نريد الأكل فقط، إذ لدينا مهام أخرى يجب تنفيذها، وسنوضّح فيما يلي كيفية إجراء استدعاءات لمكوّنات مختلفة تصيّر محتوًى فريدًا.

إنشاء مكون <Todo /‎> فريد

تُعَدّ المكونات مهمةً لأنها تتيح إعادة استخدام أجزاء من واجهة المستخدِم، والإشارة إلى مكان ما ليكون مصدرًا لواجهة المستخدِم تلك، ولكن تكمن المشكلة في أننا لا نريد إعادة استخدام جميع المكونات، وإنما نريد إعادة استخدام معظم الأجزاء، وتعديل أجزاء صغيرة، لذا يجب استخدام الخاصيات Props.

الخاصية name

إذا أردنا تتبّع أسماء المهام التي نريد إكمالها، فيجب علينا التأكد من أنّ كل مكوّن <Todo /‎> يصيِّر اسمًا فريدًا، لذا امنح كل مكوّن <Todo /‎> في الملف App.js خاصية الاسم name، ولنستخدم أسماء المهام التي كانت لدينا سابقًا كما يلي:

<Todo name="Eat" />
<Todo name="Sleep" />
<Todo name="Repeat" />

إذا حدّثتَ متصفحك، فسترى الشيء السابق نفسه بالضبط، إذ أعطينا المكوّن <Todo /‎> بعض الخاصيات، لكننا لم نستخدِمها بعد، فلنَعُد الآن إلى الملف Todo.js ونصلح كل شيء.

عدّل أولًا تعريف الدالة Todo()‎ بحيث تأخذ الخاصيات props على أساس معامِل، كما يمكنك تطبيق التابع console.log()‎ على الخاصيات props كما فعلنا سابقًا إذا أردت التحقق من استلام المكوّن للخاصيات استلامًا صحيحًا، ثم يمكنك وضع خاصية الاسم name مكان تكرارات المهمة Eat، وتذكّر استخدام الأقواس المعقوصة لحقن قيمة متغير في تعابير JSX، إذ يجب أن تكون الدالة Todo()‎ بعد ذلك كما يلي:

export default function Todo(props) {
  return (
    <li className="todo stack-small">
      <div className="c-cb">
        <input id="todo-0" type="checkbox" defaultChecked={true} />
        <label className="todo-label" htmlFor="todo-0">
          {props.name}
        </label>
      </div>
      <div className="btn-group">
        <button type="button" className="btn">
          Edit <span className="visually-hidden">{props.name}</span>
        </button>
        <button type="button" className="btn btn__danger">
          Delete <span className="visually-hidden">{props.name}</span>
        </button>
      </div>
    </li>
  );
}

يجب أن يعرض متصفحك الآن ثلاث مهام فريدة، ولكن لا تزال جميع هذه المهام مُحدَّدةً افتراضيًا.

02_todo-list-unique-todos.png

الخاصية completed

حُدِّدت المهمة Eat فقط في القائمة الثابتة الأصلية، إذ نريد إعادة استخدام معظم واجهة المستخدِم التي تشكل المكوِّن <Todo /‎‎‎> مع تعديل شيء واحد من خلال منح كل استدعاء للمكوّن <Todo /‎‎‎> في الملف App.js الخاصية completed الجديدة، كما يجب أن يكون للخاصية completed التابعة للمكوّن الأول الذي اسمه Eat القيمة true، وللمكونات الأخرى القيمة false كما يلي:

<Todo name="Eat" completed={true} />
<Todo name="Sleep" completed={false} />
<Todo name="Repeat" completed={false} />

يجب علينا العودة إلى الملف Todo.js لاستخدام هذه الخاصيات، لذا عدّل السمة defaultChecked للعنصر <input /‎> بحيث تساوي قيمتها الخاصية completed، ثم يكون عنصر <input /‎> الخاص بالمكون Todo على النحو التالي:

<input id="todo-0" type="checkbox" defaultChecked={props.completed} />

حدّث متصفحك لإظهار تحديد المكوِّن Eat فقط كما يلي:

03_todo-list-differing-checked-states.png

إذا عدّلت كل خاصيات completed الخاصة بالمكوّن <Todo /‎>، فسيحدِّد متصفحك أو يلغي تحديد مربعات الاختيار المكافئة والمُصيَّرة وفقًا لذلك.

الخاصية id

يعطي المكوّن <Todo /‎> لكل مهمة السمة id بالقيمة todo-0، وهذا خطأ في HTML لأن سمات id يجب أن تكون فريدةً، إذ تستخدِمها لغات جافاسكربت وCSS وغيرها بوصفها معرّفات فريدةً لأجزاء المستند، وبالتالي يجب أن نعطي المكون الخاصية id التي تأخذ قيمةً فريدةً لكل مكوّن Todo.

إذًا لنمنح كل نسخة من المكوِّن <Todo /‎> معرّفًا باستخدام التنسيق todo-i، إذ تزيد قيمة i بمقدار واحد في كل مرة كما يلي:

<Todo name="Eat" completed={true} id="todo-0" />
<Todo name="Sleep" completed={false} id="todo-1" />
<Todo name="Repeat" completed={false} id="todo-2" />

عُد الآن إلى الملف Todo.js واستفد من الخاصية id، إذ يجب تعديل قيمة السمة id للعنصر <input /‎> وقيمة السمة htmlFor الخاصة بالعنصر label كما يلي:

<div className="c-cb">
  <input id={props.id} type="checkbox" defaultChecked={props.completed} />
  <label className="todo-label" htmlFor={props.id}>
    {props.name}
  </label>
</div>

كل شيء جيد حتى الآن، ولكن تُعَدّ شيفرتنا مكرَّرةً، فالأسطر الثلاثة التي تصيّر المكوّن <Todo /‎> متطابقة تقريبًا مع اختلاف واحد فقط هو قيمة كل خاصية، كما يمكننا تنظيف شيفرتنا باستخدام إحدى ميزات جافاسكربت الأساسية وهي التكرار Iteration، ولكن يجب أولًا إعادة التفكير في المهام لاستخدام هذه الميزة.

بيانات المهام

تحتوي كل مهمة من مهامنا حاليًا على ثلاثة أجزاء من المعلومات، وهي اسمها، وما إذا كانت مُحدَّدة، ومعرّفها الفريد، كما تُترجَم هذه البيانات إلى كائن Object، وبما أنه لدينا أكثر من مهمة، فسنستخدِم مصفوفةً من الكائنات لتمثيل هذه البيانات، لذا أنشئ ثابتًا const جديدًا بعد تعليمة الاستيراد الأخيرة في الملف src/index.js وقبل التابع ReactDOM.render()‎ كما يلي:

const DATA = [
  { id: "todo-0", name: "Eat", completed: true },
  { id: "todo-1", name: "Sleep", completed: false },
  { id: "todo-2", name: "Repeat", completed: false }
];

سنمرِّر بعد ذلك الثابت DATA إلى المكوّن <App /‎> بوصفه خاصيةً تُسمَّى tasks، إذ يجب أن يكون السطر الأخير من الملف src/index.js كما يلي:

ReactDOM.render(<App tasks={DATA} />, document.getElementById("root"));

أصبحت هذه المصفوفة متاحةً الآن للمكون App بالصورة props.tasks، كما يمكنك استخدام التابع console.log()‎ للتحقق من ذلك.

اقتباس

ملاحظة: ليس لأسماء الثوابت ذات الأحرف الكبيرة ALL_CAPS معنًى خاصًا في جافاسكربت، وإنما هي اتفاقية بين المطوِّرين تخبرهم بعدم تغيّر هذه البيانات بعد تعريفها أبدًا.

التصيير مع التكرار

يمكننا تصيير مصفوفة الكائنات من خلال تحويل كل منها إلى المكون <Todo /‎>، إذ تمنحنا لغة جافاسكربت تابع مصفوفة لتحويل البيانات إلى شيء آخر، وهو Array.prototype.map()‎، لذا أنشئ ثابتًا const جديدًا يسمى taskList قبل تعليمة return الخاصة بالدالة App()‎، واستخدِم التابع map()‎ لتحويله، ولنحوّل مجموعة مهامنا إلى شيء بسيط يتمثّل باسم name كل مهمة كما يلي:

const taskList = props.tasks?.map(task => task.name);

لنحاول وضع الثابت taskList مكان جميع أبناء العنصر <ul> كما يلي:

<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading"
>
  {taskList}
</ul>

يمنحنا ذلك طريقةً لإظهار جميع المكوّنات مرةً أخرى، إذ يصيّر المتصفح حاليًا اسم كل مهمة بوصفه نصًا دون بنية معينة، كما ينقصنا حاليًا بنية HTML مثل عنصر <li> ومربعات الاختيار والأزرار.

04_todo-list-unstructured-names.png

يمكننا إصلاح ذلك من خلال إعادة المكوّن <Todo /‎> من التابع map()‎، وتذكَّر أنّ صيغة JSX تسمح لنا بخلط بنى جافاسكربت مع اللغات التوصيفية Markup، فلنجرب ما يلي بدلًا مما لدينا حاليًا:

const taskList = props.tasks.map(task => <Todo />);

انظر مرةً أخرى إلى تطبيقك، إذ تبدو مهامنا الآن كما كانت سابقًا، لكنها تفتقد إلى أسماء المهام نفسها، وتذكَّر أنّ كل مهمة نطبّق عليها التابع map()‎ لها الخاصيات id وname وchecked، والتي نريد تمريرها إلى المكوّن <Todo /‎>، وبالتالي سنحصل على الشيفرة التالية:

const taskList = props.tasks.map(task => (
  <Todo id={task.id} name={task.name} completed={task.completed} />
));

يبدو التطبيق الآن كما كان سابقًا، ولكن أصبحت شيفرتنا أقل تكرارًا.

خاصيات key الفريدة

يجب أن تتعقّب React المهام لتصييرها بصورة صحيحة بعد أن صيّرت هذه المهام من مصفوفة، إذ تستخدِم React التخمين لتتبع الأشياء، ولكن يمكننا مساعدتها عن طريق تمرير الخاصية key لمكونات <Todo /‎>، إذ تُعَدّ key خاصيةً خاصةً تديرها React، ولا يمكنك استخدام الكلمة key لأيّ غرض آخر، وبما أنّ الخاصية key يجب أن تكون فريدةً، فسنعيد استخدام خاصية id الخاصة بكل كائن مهمة على أساس مفتاح له، لذا عدِّل الثابت taskList كما يلي:

const taskList = props.tasks.map(task => (
    <Todo
      id={task.id}
      name={task.name}
      completed={task.completed}
      key={task.id}
    />
  )
);

يجب عليك دائمًا تمرير مفتاح فريد لأيّ شيء تُصيّره مع التكرار، ولن يتغير شيء واضح في متصفحك، ولكن إذا لم تستخدِم مفاتيح فريدةً، فستعطي React تحذيرات في الطرفية console ويمكن أن يتصرف تطبيقك بطريقة غريبة.

تقسيم أجزاء التطبيق المتبقية إلى مكونات

يمكننا الآن تحويل باقي التطبيق إلى مكونات بعد فرزِنا المكوّن الأكثر أهميةً، وتذكَّر أنّ المكونات هي إما أجزاء واضحة من واجهة المستخدِم، أو أجزاء معاد استخدامها من واجهة المستخدِم، أو كليهما، كما يمكننا إنشاء مكونين آخرين هما:

  • <Form/‎>
  • <FilterButton/‎>

بما أننا نعلم بحاجتنا لهذين المكوِنين، فيمكننا تجميع أوامر إنشاء الملفات في أمر واحد في الطرفية، لذا شغّل الأمر التالي في طرفيتك، مع الانتباه إلى أنك في المجلد الجذر لتطبيقك:

touch src/components/Form.js src/components/FilterButton.js

المكون <Form/‎>

افتح الملف components/Form.js ونفِّذ ما يلي:

  • استورد مكتبة React في أعلى الملف كما فعلنا في الملف Todo.js.
  • أنشئ المكوِّن Form()‎ الجديد باستخدام بنية Todo()‎ الأساسية نفسها، ثم صدِّر هذا المكوِّن.
  • انسخ وسوم <form> وما يوجد بينها من الملف App.js، والصقها ضمن تعليمة return الخاصة بالمكوِّن Form()‎.
  • صدّر المكوِّن Form في نهاية الملف.

يجب أن يكون الملف Form.js الآن كما يلي:

import React from "react";

function Form(props) {
  return (
    <form>
      <h2 className="label-wrapper">
        <label htmlFor="new-todo-input" className="label__lg">
          What needs to be done?
        </label>
      </h2>
      <input
        type="text"
        id="new-todo-input"
        className="input input__lg"
        name="text"
        autoComplete="off"
      />
      <button type="submit" className="btn btn__primary btn__lg">
        Add
      </button>
    </form>
  );
}

export default Form;

المكون <FilterButton/‎>

كرِّر الأمور نفسها التي نفّذتها لإنشاء الملف Form.js على الملف FilterButton.js، ولكن استدعِ المكوِّن FilterButton()‎ وانسخ جزء HTML للزر الأول الموجود ضمن العنصر <div> ذو الصنف filters من الملف App.js في تعليمة return، إذ يجب أن يكون الملف الآن كما يلي:

import React from "react";

function FilterButton(props) {
  return (
    <button type="button" className="btn toggle-btn" aria-pressed="true">
      <span className="visually-hidden">Show </span>
      <span>all </span>
      <span className="visually-hidden"> tasks</span>
    </button>
  );
}

export default FilterButton;
اقتباس

ملاحظة: لاحظ أننا ارتكبنا الخطأ نفسه هنا الذي ارتكبناه لأول مرة مع المكوِّن <Todo /‎>، إذ سيبقى كل زر كما هو، ولكن سنصلح هذا المكوِّن لاحقًا.

استيراد جميع المكونات

أضف بعض تعليمات الاستيراد import في الجزء العلوي من الملف App.js لاستيراد هذه المكوّنات الجديدة، ثم عدّل تعليمة return الخاصة بالمكوِّن App()‎ لتصيير المكونات، إذ يجب أن يكون الملف ‎‎App.js كما يلي:

import React from "react";
import Form from "./components/Form";
import FilterButton from "./components/FilterButton";
import Todo from "./components/Todo";

function App(props) {
  const taskList = props.tasks.map(task => (
    <Todo
        id={task.id}
        name={task.name}
        completed={task.completed}
        key={task.id}
      />
    )
  );
  return (
    <div className="todoapp stack-large">
      <h1>TodoMatic</h1>
      <Form />
      <div className="filters btn-group stack-exception">
        <FilterButton />
        <FilterButton />
        <FilterButton />
      </div>
      <h2 id="list-heading">3 tasks remaining</h2>
      <ul
        role="list"
        className="todo-list stack-large stack-exception"
        aria-labelledby="list-heading"
      >
        {taskList}
      </ul>
    </div>
  );
}

export default App;

نكون بذلك جاهزين تقريبًا للتعامل مع التفاعل في تطبيق React الخاص بنا.

الخلاصة

تعمّقنا في كيفية تقسيم التطبيق إلى مكوّنات وتصييرها بكفاءة، إذ سننتقل الآن إلى إلقاء نظرة على كيفية التعامل مع الأحداث في React وإضافة التفاعل.

ترجمة -وبتصرُّف- للمقال Componentizing our React app.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...