Kodax

الأحداث والعرض الشرطي والقوائم — مقدمة React

الأحداث والعرض الشرطي والقوائم

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

الأحداث

function LikeButton() {
  function handleClick(event) {
    console.log("نقرة على", event.currentTarget);
  }

  return <button type="button" onClick={handleClick}>أعجبني</button>;
}

- أسماء الأحداث camelCase: onClick، onChange، onSubmit، onKeyDown.

- مرّر الدالة نفسها لا نتيجة استدعائها:

- ✅ onClick={handleClick}

- ❌ onClick={handleClick()} — تُستدعى أثناء الرسم فوراً (وغالباً تسبب حلقة إعادة رسم لا نهائية إن غيّرت الحالة).

- لتمرير معامل استخدم دالة سهمية: onClick={() => remove(item.id)}.

منع السلوك الافتراضي

function SearchForm({ onSearch }) {
  function handleSubmit(e) {
    e.preventDefault();                 // بدونها يعيد المتصفح تحميل الصفحة
    const query = new FormData(e.currentTarget).get("q");
    onSearch(query);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="q" aria-label="بحث" />
      <button type="submit">بحث</button>
    </form>
  );
}

استخدم onSubmit على النموذج بدل onClick على الزر: يعمل مع زر Enter أيضاً.

العرض الشرطي

1) if مع إرجاع مبكر

function Profile({ user, loading }) {
  if (loading) return <p>جارٍ التحميل…</p>;
  if (!user) return <p>سجّل الدخول أولاً</p>;
  return <h2>مرحباً {user.name}</h2>;
}

2) العامل الثلاثي داخل JSX

function Status({ online }) {
  return <span>{online ? "متصل 🟢" : "غير متصل"}</span>;
}

3) && لإظهار شيء أو لا شيء

function Inbox({ messages }) {
  return (
    <div>
      {messages.length > 0 && <p>لديك {messages.length} رسائل</p>}
    </div>
  );
}

⚠️ فخ الصفر: {messages.length && <p>...</p>} يطبع 0 على الشاشة عندما تكون القائمة فارغة، لأن React يعرض الأرقام. اجعل الطرف الأيسر منطقياً دائماً: messages.length > 0 &&.

القوائم بـ map

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

map تحوّل مصفوفة بيانات إلى مصفوفة عناصر. ولتصفية قبل العرض: todos.filter(t => !t.done).map(...).

لماذا key؟

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

قواعد المفتاح:

- فريد بين الإخوة (ليس في الصفحة كلها).

- ثابت: نفس العنصر يحمل نفس المفتاح في كل رسم.

- يأتي من البيانات: id من قاعدة البيانات أو crypto.randomUUID() عند الإنشاء.

لماذا لا `key={index}`؟

إذا حذفت العنصر الأول، يصبح فهرس كل العناصر التالية مختلفاً، فيظن React أن العنصر 0 ما زال موجوداً وغيّر محتواه — فتنتقل الحالة الداخلية (نص حقل، مربع اختيار محدد) إلى العنصر الخطأ. الفهرس مقبول فقط لقائمة ثابتة لا تتغير أبداً ولا تحتوي حالة.

ولا تولّد المفتاح أثناء الرسم (key={Math.random()}): مفتاح جديد كل مرة يعني أن React يهدم كل العناصر ويبنيها من جديد.

التفاف عدة عناصر: Fragment

المكوّن يعيد عنصراً جذرياً واحداً. لتجنب div إضافي:

import { Fragment } from "react";

function Glossary({ items }) {
  return (
    <dl>
      {items.map((item) => (
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.meaning}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

<>...</> اختصار لـ Fragment، لكنه لا يقبل key — في القوائم استخدم <Fragment key>.

تمرين

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

import { useState } from "react";

const INITIAL = [
  { id: "c1", name: "سارة", phone: "0770 111 2222" },
  { id: "c2", name: "عمر", phone: "0780 333 4444" },
  { id: "c3", name: "ليلى", phone: "0750 555 6666" },
];

export default function Contacts() {
  const [contacts, setContacts] = useState(INITIAL);
  const [query, setQuery] = useState("");

  const visible = contacts.filter((c) => c.name.includes(query.trim()));

  function remove(id) {
    setContacts((prev) => prev.filter((c) => c.id !== id));
  }

  function handleAdd(e) {
    e.preventDefault();
    const name = new FormData(e.currentTarget).get("name").trim();
    if (!name) return;
    setContacts((prev) => [...prev, { id: crypto.randomUUID(), name, phone: "—" }]);
    e.currentTarget.reset();
  }

  return (
    <section>
      <h2>جهات الاتصال ({contacts.length})</h2>

      <input
        type="search"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="ابحث بالاسم"
        aria-label="ابحث بالاسم"
      />

      {visible.length === 0 ? (
        <p>لا توجد نتائج لـ "{query}"</p>
      ) : (
        <ul>
          {visible.map((c) => (
            <li key={c.id}>
              <strong>{c.name}</strong> — <span dir="ltr">{c.phone}</span>
              <button type="button" onClick={() => remove(c.id)} aria-label={`حذف ${c.name}`}>
                ✕
              </button>
            </li>
          ))}
        </ul>
      )}

      <form onSubmit={handleAdd}>
        <input name="name" aria-label="اسم جديد" placeholder="اسم جديد" />
        <button type="submit">إضافة</button>
      </form>
    </section>
  );
}
اختبر فهمك