العودة للمدونة

البدء مع React

البدء مع React

تعلّم React ببناء قائمة مهام كاملة: المكونات وJSX وprops وuseState، النماذج المتحكَّم بها، المفاتيح الثابتة، وتحديث البيانات مع مثال App.jsx وخطوات فحص واضحة.

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

ماذا تحتاج قبل تعلّم React؟

ابدأ بمعرفة أساسية بلغة HTML: العناوين والقوائم والنماذج والأزرار، وطريقة ربط تسمية بحقل إدخال. تعلّم من CSS ما يكفي لضبط المسافات وعرض العناصر، ومن JavaScript الدوال والكائنات والمصفوفات والاستيراد وتفكيك القيم. ستحتاج خصوصًا إلى فهم map وfilter: الأولى تبني قائمة نتائج، والثانية تختار عناصر تحقق شرطًا. جرّبهما في مصفوفة أسماء قبل متابعة المثال إذا كانت طريقة عملهما جديدة عليك.

تساعدك React على وصف الواجهة انطلاقًا من البيانات الحالية؛ عندما تتغير البيانات، تحسب React العرض المناسب. تبقى دلالات HTML وقواعد CSS مهمة. ولتضع هذا الجزء ضمن الصورة الأوسع، راجع دليل تطوير الويب الحديث الذي يربط بين المتصفح والخادم وقاعدة البيانات. تطبيقنا هنا يعمل داخل المتصفح فقط، ولا يتضمن تسجيل دخول أو اتصالًا بقاعدة بيانات.

تجهيز مشروع تعليمي باستخدام Vite

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

ثبّت إصدار LTS مدعومًا من Node.js يوافق متطلبات Vite الحالية. افتح الطرفية في مجلد مناسب لمشروعات التدريب، ثم نفّذ الأوامر التالية واحدًا بعد آخر. إذا عرضت أداة الإنشاء تثبيت الحزم وتشغيل المشروع فورًا، ارفض هذه الخطوة الاختيارية وأكمل الأوامر الصريحة أدناه.

node --version
npm create vite@latest react-task-board -- --template react
cd react-task-board
npm install
npm run dev

افتح العنوان المحلي الذي تعرضه الطرفية، واترك الخادم يعمل أثناء التعديل. يمكنك إيقافه لاحقًا باستخدام Ctrl+C. القالب react يستخدم JavaScript وJSX. افتح الملف src/App.jsx؛ سنستبدل محتواه كاملًا بالمثال، مع إبقاء ملف src/main.jsx الذي أنشأه القالب. لا تحتاج إلى حزمة أو ملف تنسيق إضافي. قد تؤثر تنسيقات Vite الأولية في الشكل، لكنها لا تغيّر منطق التمرين.

المكوّن وJSX وخصائص props

قسّم الفكرة إلى مكوّن رئيسي اسمه App يمثل اللوحة، ومكوّن TaskItem يمثل سطرًا واحدًا. تبدأ أسماء المكونات بحرف كبير. نستخدم JSX لوصف ما تعيده، ونضع تعبيرات JavaScript داخل الأقواس المعقوفة. أغلق الوسوم، واستخدم className للفئات، واجمع العناصر المتجاورة تحت عنصر أب أو Fragment. يشرح دليل JSX الرسمي قواعد الصياغة.

يتلقى السطر كائن المهمة ودالتين عبر props. وظيفته عرض المهمة وإبلاغ الأب عندما يطلب المستخدم إكمالها أو حذفها. تبقى القائمة عند المكوّن الأب، فتكون الجهة المسؤولة عن تعديلها واضحة. خصائص props مدخلات للقراءة؛ راجع شرح تمرير الخصائص. لاحظ أن تعريف TaskItem خارج App، حتى لا يُنشأ نوع مكوّن جديد عند كل تحديث للوحة.

ملف App.jsx كاملًا

انسخ الكود التالي كاملًا داخل src/App.jsx واحفظ الملف. يبدأ المثال بمهمتين، إحداهما مكتملة، لتجربة الفلاتر مباشرة. النصوص عربية، واتجاه اللوحة من اليمين إلى اليسار، بينما تبقى أسماء متغيرات البرمجة بالإنجليزية لتسهيل تتبعها في الشرح.

import { useState } from "react";

const initialTasks = [
  { id: "read-components", text: "قراءة فصل عن المكونات", done: false },
  { id: "try-board", text: "تجربة قائمة المهام", done: true },
];

function TaskItem({ task, onToggle, onRemove }) {
  return (
    <li>
      <label>
        <input
          type="checkbox"
          checked={task.done}
          onChange={() => onToggle(task.id)}
        />
        {" "}
        {task.done ? <s>{task.text}</s> : task.text}
      </label>
      {" "}
      <button
        type="button"
        onClick={() => onRemove(task.id)}
        aria-label={"حذف: " + task.text}
      >
        حذف
      </button>
    </li>
  );
}

export default function App() {
  const [tasks, setTasks] = useState(initialTasks);
  const [draft, setDraft] = useState("");
  const [filter, setFilter] = useState("all");
  const [error, setError] = useState("");

  const completedCount = tasks.filter((task) => task.done).length;
  const visibleTasks = tasks.filter((task) =>
    filter === "all" ? true : filter === "done" ? task.done : !task.done
  );

  function handleAdd(event) {
    event.preventDefault();
    const text = draft.trim();
    if (!text) {
      setError("اكتب اسمًا للمهمة قبل إضافتها.");
      return;
    }
    const newTask = { id: crypto.randomUUID(), text, done: false };
    setTasks((currentTasks) => [...currentTasks, newTask]);
    setDraft("");
    setError("");
  }

  function toggleTask(id) {
    setTasks((currentTasks) =>
      currentTasks.map((task) =>
        task.id === id ? { ...task, done: !task.done } : task
      )
    );
  }

  function removeTask(id) {
    setTasks((currentTasks) =>
      currentTasks.filter((task) => task.id !== id)
    );
  }

  return (
    <main lang="ar" dir="rtl" style={{ maxWidth: "40rem", padding: "1.5rem", textAlign: "start" }}>
      <h1>مهامي الصغيرة</h1>
      <p>إجمالي المهام: {tasks.length} — المكتملة: {completedCount}</p>
      <form onSubmit={handleAdd}>
        <label htmlFor="new-task">مهمة جديدة</label>
        {" "}
        <input
          id="new-task"
          value={draft}
          maxLength={120}
          onChange={(event) => {
            setDraft(event.target.value);
            setError("");
          }}
          aria-invalid={error ? true : undefined}
          aria-describedby={error ? "task-error" : undefined}
        />
        {" "}
        <button type="submit">إضافة المهمة</button>
        {error ? <p id="task-error" role="alert">{error}</p> : null}
      </form>
      <p>
        <label htmlFor="task-filter">عرض المهام</label>
        {" "}
        <select
          id="task-filter"
          value={filter}
          onChange={(event) => setFilter(event.target.value)}
        >
          <option value="all">الكل</option>
          <option value="open">المتبقية</option>
          <option value="done">المكتملة</option>
        </select>
      </p>
      {visibleTasks.length > 0 ? (
        <ul>
          {visibleTasks.map((task) => (
            <TaskItem
              key={task.id}
              task={task}
              onToggle={toggleTask}
              onRemove={removeTask}
            />
          ))}
        </ul>
      ) : (
        <p>لا توجد مهام في هذا العرض.</p>
      )}
      <p>تُحفظ المهام في هذه الجلسة فقط.</p>
    </main>
  );
}

تتبّع رحلة إضافة مهمة

اكتب «مراجعة نموذج التواصل» في الحقل. يستقبل معالج التغيير النص الحالي ويستدعي setDraft، ثم تظهر القيمة الجديدة في الحقل بعد تحديث الواجهة. عند الإرسال تُستدعى handleAdd، وتمنع preventDefault انتقال المتصفح المعتاد عند إرسال النموذج. نحذف الفراغات المحيطة بالنص، ونرفض النتيجة الفارغة، ثم ننشئ مهمة ونضيفها. أخيرًا نمسح المسودة ورسالة الخطأ. جرّب الإرسال بمفتاح Enter أيضًا، لتفهم لماذا ربطنا العملية بالنموذج نفسه.

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

حدّث المصفوفات من دون تعديل النسخة القديمة

عند الإضافة نبني مصفوفة جديدة باستخدام الانتشار. عند الحذف تنشئ filter مصفوفة لا تحتوي المعرّف المطلوب. وعند تبديل الاكتمال تمر map على المهام، وتستبدل المهمة المطابقة بكائن جديد ذي قيمة done مختلفة. تجنّب tasks.push(...) أو الكتابة المباشرة إلى task.done. يشرح دليل تحديث المصفوفات هذا الأسلوب.

استخدمنا الصيغة setTasks((currentTasks) => ...) لأن النتيجة تعتمد على القائمة السابقة. تصل الدالة إلى أحدث حالة في طابور التحديثات، ثم تعيد القائمة التالية. يجب أن تبقى هذه العملية حسابًا نقيًا؛ لذلك أنشأنا المعرّف قبل دخولها، داخل معالج الحدث. قد تعيد فحوص التطوير استدعاء دالة التحديث، فلا تضع فيها إرسال طلب أو تغيير بيانات خارجية. أما اختيار فلتر جديد فيستطيع تمرير القيمة مباشرة.

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

المفاتيح الثابتة والعرض الشرطي

تعرض القائمة كل سطر باستخدام key={task.id}. المعرّف يعبّر عن هوية المهمة حتى بعد تغيير الفلتر؛ أما فهرس المصفوفة فيعبّر عن موقع قد يتبدل. ولا تنشئ مفتاحًا عشوائيًا أثناء العرض، لأنه يتغير مع كل مرة. يشرح مرجع عرض القوائم أهمية المفاتيح الثابتة. يمكن أن تحمل مهمتان الاسم نفسه وتبقيا عنصرين مستقلين.

يعرض الشرط قائمة عندما توجد نتائج، ورسالة واضحة عندما تخلو النتائج. احذف المهام المكتملة ثم اختر فلتر «المكتملة» لتجربته. لاحظ أن إجمالي المهام يشمل القائمة كلها، بينما يعرض الفلتر جزءًا منها فقط. هذا سلوك مقصود. وفحص length > 0 يجعل قرار العرض واضحًا ويتجنب ظهور الرقم صفر عرضًا بدل الرسالة المطلوبة.

نموذج متحكَّم به وبيانات مشتقة بلا Effect

يرتبط حقل النص بـvalue وonChange معًا؛ حذف معالج التغيير يمنع التحرير المعتاد. تبدأ المسودة بنص فارغ كي يبقى الحقل متحكَّمًا به منذ البداية. التسمية مرئية، والخطأ مرتبط بالحقل، ومربع الاختيار يستخدم checked للقيمة المنطقية. راجع مرجع حقول React لتفاصيل هذا الارتباط.

عدد المهام المكتملة والقائمة المرئية قيمتان محسوبتان من الحالة الموجودة. تخزينهما منفصلتين يفرض مزامنتهما بعد كل إضافة وحذف وتبديل. لا نحتاج إلى Effect لهذه الحسابات؛ يشرح دليل الحالات التي لا تحتاج إلى Effect هذا الفرق. في قائمة صغيرة، يكفي الترشيح المباشر. أضف التعقيد لتحسين مشكلة مقاسة، لا لأنك رأيت اسم Hook جديدًا.

اختبر السلوك قبل توسيع المشروع

  1. أضف نصًا عاديًا، ثم أرسل فراغات فقط. يجب أن تضيف المحاولة الأولى مهمة وتعرض الثانية رسالة خطأ.
  2. أنشئ مهمتين بالاسم نفسه، وأكمل واحدة فقط للتأكد من استقلال هويتهما.
  3. بدّل الفلاتر الثلاثة، واحذف مهمة في عرض مفلتر، ثم راقب العدادات.
  4. استخدم Tab وEnter وSpace للتنقل وتشغيل الحقول والأزرار ومربعات الاختيار.
  5. أعد تحميل الصفحة. ستعود المهمتان الأصليتان لأن التخزين هنا في الذاكرة فقط.

بعد ذلك تأكد من إنشاء نسخة الإنتاج ومعاينتها محليًا:

npm run build
npm run preview

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

أخطاء شائعة وخطوتك التالية

  • صفحة فارغة: اقرأ أول خطأ في Console ورسالة الطرفية، وافحص الوسوم وأسماء الاستيراد والتصدير الافتراضي.
  • تحديثات متكررة بلا توقف: مرّر دالة إلى الحدث مثل onClick={() => onRemove(task.id)}، ولا تستدعِ دالة تحديث أثناء العرض.
  • حقل لا يقبل الكتابة: افحص معالج التغيير وقيمة الحالة؛ التعديل اليدوي لعنصر DOM يتعارض مع القيمة المتحكَّم بها.
  • غياب crypto.randomUUID: استخدم عنوان التطوير المحلي أو HTTPS مع متصفح حديث؛ المثال موجّه لهذه البيئات.
  • مهمة جديدة غير ظاهرة: فلتر «المكتملة» يخفي المهام الجديدة غير المكتملة. ارجع إلى «الكل» قبل افتراض فشل الإضافة.

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