طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 242 · أطر الويب
الدرس 3 · الوحدة 1: إتقان الهندسة المعتمدة على المكوّنات

الحالة والأحداث: useState

كيف «يتذكر» المكوّن ويتفاعل مع المستخدم؟ ⏱ نحو 35 دقيقة.

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

١لماذا لا يكفي متغير عادي؟

نريد زرًّا يزيد عدّادًا. جرّب الطريقة البديهية أولًا: متغير عادي نزيده عند الضغط:

✗ خطأ
export default function App() {  let count = 0;  return (    <button onClick={() => { count++; console.log(count); }}>      ضغطت {count} مرات    </button>  );}
✓ صواب
import { useState } from 'react'; export default function App() {  const [count, setCount] = useState(0);  return (    <button onClick={() => setCount(count + 1)}>      ضغطت {count} مرات    </button>  );}

اضغط الزرين. في الأول يزيد المتغير (انظر Console) لكن الشاشة لا تتغير، لأن React لا يعرف أن شيئًا تغيّر. وفي الثاني يُحدَّث العرض فورًا.

الحالة State: بيانات يملكها المكوّن وتتغير مع الوقت، وكلما تغيّرت أعاد React رسم المكوّن بالقيمة الجديدة. والدالة useState تعطيك زوجًا: القيمة الحالية، ودالة لتغييرها.

const [count, setCount] = useState(0);//     القيمة   دالة التغيير      القيمة الأولى

٢الأحداث في React

نربط الأحداث بسمات مثل onClick و onChange و onSubmit، ونمرر دالة لا استدعاء دالة:

الصياغةمعناها
onClick={handleClick}✓ مرّر الدالة؛ React يستدعيها عند الضغط.
onClick={() => setCount(count + 1)}✓ دالة سهمية تستدعي ما تريد عند الضغط.
onClick={setCount(count + 1)}✗ تُستدعى فورًا أثناء الرسم، فتغيّر الحالة، فيُعاد الرسم… حلقة لا تنتهي!
import { useState } from 'react'; export default function App() {  const [likes, setLikes] = useState(0);  const [liked, setLiked] = useState(false);   function toggle() {    setLiked(!liked);    setLikes(liked ? likes - 1 : likes + 1);  }   return (    <div>      <button onClick={toggle} style={{ fontSize: 20 }}>{liked ? '❤️' : '🤍'} أعجبني</button>      <span> {likes} إعجاب</span>      <p>{liked ? 'شكرًا لإعجابك!' : 'هل أعجبك الدرس؟'}</p>    </div>  );}

٣الحقول المتحكَّم بها

في React نربط قيمة الحقل بالحالة: الحالة هي مصدر القيمة، و onChange يحدّثها مع كل حرف. هذا اسمه الحقل المتحكَّم به Controlled input، ويتيح لك أن تعرف القيمة دائمًا وتتحقق منها فورًا:

import { useState } from 'react'; export default function App() {  const [name, setName] = useState('');  const tooShort = name.length > 0 && name.length < 3;   return (    <div>      <label>الاسم: <input value={name} onChange={e => setName(e.target.value)} /></label>      {tooShort && <p style={{ color: 'crimson' }}>الاسم قصير</p>}      <p>مرحبًا {name || 'يا زائر'} ({name.length} حروف)</p>    </div>  );}

اكتب في الحقل داخل المعاينة. e.target.value هي القيمة الجديدة للحقل.

والنموذج كاملًا: نمنع سلوك المتصفح الافتراضي (إعادة تحميل الصفحة) بـ e.preventDefault()، ثم نعالج البيانات بأنفسنا:

import { useState } from 'react'; export default function App() {  const [email, setEmail] = useState('');  const [sent, setSent] = useState('');   function handleSubmit(e) {    e.preventDefault();    setSent(email);    setEmail('');  }   return (    <form onSubmit={handleSubmit}>      <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="بريدك" />      <button>اشترك</button>      {sent && <p>✓ اشترك {sent} في النشرة</p>}    </form>  );}

٤تحديث المصفوفات والكائنات: نسخة جديدة دائمًا

React يقارن القيمة القديمة بالجديدة ليعرف هل تغيّرت. فإن عدّلت المصفوفة نفسها (push) بقيت هي هي، فلا يُعاد الرسم. القاعدة: لا تعدّل الحالة، بل أنشئ نسخة جديدة:

العملية✗ تعديل مباشر✓ نسخة جديدة
إضافةlist.push(x)setList([...list, x])
حذفlist.splice(i, 1)setList(list.filter(t => t.id !== id))
تعديل عنصرlist[i].done = truesetList(list.map(t => t.id === id ? {...t, done: true} : t))
كائنuser.name = 'ريم'setUser({...user, name: 'ريم'})
import { useState } from 'react'; export default function App() {  const [tasks, setTasks] = useState([    { id: 1, text: 'مراجعة الدرس', done: false },  ]);  const [text, setText] = useState('');   function add() {    if (!text.trim()) return;    setTasks([...tasks, { id: Date.now(), text, done: false }]);    setText('');  }  function toggle(id) {    setTasks(tasks.map(t => t.id === id ? { ...t, done: !t.done } : t));  }  function remove(id) {    setTasks(tasks.filter(t => t.id !== id));  }   return (    <div>      <input value={text} onChange={e => setText(e.target.value)} placeholder="مهمة جديدة" />      <button onClick={add}>أضف</button>      <ul>        {tasks.map(t => (          <li key={t.id}>            <span onClick={() => toggle(t.id)} style={{ textDecoration: t.done ? 'line-through' : 'none', cursor: 'pointer' }}>{t.text}</span>            {' '}<button onClick={() => remove(t.id)}>✕</button>          </li>        ))}      </ul>      <p>المنجز: {tasks.filter(t => t.done).length} من {tasks.length}</p>    </div>  );}

أضف مهام، واضغط نص المهمة لتعليمها منجزة، و ✕ لحذفها. لاحظ أن «المنجز» يتحدث وحده لأنه محسوب من الحالة، لا حالة مستقلة.

٥الحالة لكل نسخة من المكوّن، والتحديث بالدالة

كل نسخة من المكوّن لها حالتها المستقلة. هنا ثلاثة عدّادات من المكوّن نفسه، وكل واحد يتذكر قيمته:

import { useState } from 'react'; function Counter({ label }) {  const [n, setN] = useState(0);  return (    <p>      {label}: <b>{n}</b>{' '}      <button onClick={() => setN(n + 1)}>+1</button>{' '}      <button onClick={() => { setN(c => c + 1); setN(c => c + 1); }}>+2</button>    </p>  );} export default function App() {  return (    <>      <Counter label="القهوة" />      <Counter label="الشاي" />      <Counter label="العصير" />    </>  );}

زر +2 يستخدم التحديث بالدالة setN(c => c + 1): تأخذ القيمة الأحدث كل مرة. لو كتبت setN(n + 1) مرتين لزاد واحدًا فقط، لأن n في الرسم الحالي ثابتة. استخدم صيغة الدالة حين تعتمد القيمة الجديدة على السابقة.

توقّع وتحقّق

ماذا يحدث حين تستدعي دالة تغيير الحالة؟

هذا معنى الحالة.

الطريقة الصحيحة لإضافة عنصر إلى حالة مصفوفة:

نسخة جديدة.

في الحقل المتحكَّم به، مصدر القيمة هو:

value={state}

لمنع إعادة تحميل الصفحة عند إرسال النموذج:

في onSubmit.

الخلاصة

تحدٍّ برمجي: قائمة الحضور

متوسط

المدرب يسجّل أسماء الحاضرين في قائمة، ويستطيع حذف اسم أُضيف بالخطأ.

المطلوب:
  • حقل <input> متحكَّم به، وزر «أضف» يضيف الاسم إلى قائمة <ul> ثم يفرّغ الحقل.
  • لا يُضاف الاسم الفارغ (أو المسافات فقط).
  • كل اسم في <li> ومعه زر «حذف» يحذفه.
  • تحت القائمة: <p className="total">الحاضرون: 2</p> محسوبًا من القائمة.
👀 اعرض الناتج المطلوب (وجرّبه)
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء في Console
  • في البداية: الحاضرون: 0
  • إضافة اسمين تظهرهما في القائمة
  • الحقل يُفرَّغ بعد الإضافة
  • الاسم الفارغ لا يُضاف
  • زر «حذف» يحذف اسمه فقط

💡 تلميح 1
حالتان: نص الحقل، ومصفوفة الأسماء.
💡 تلميح 2
الحذف: setList(list.filter(x => x.id !== p.id))

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

import { useState } from 'react'; export default function App() {  const [name, setName] = useState('');  const [list, setList] = useState([]);   function add() {    if (!name.trim()) return;    setList([...list, { id: Date.now() + Math.random(), name: name.trim() }]);    setName('');  }   return (    <div>      <input value={name} onChange={e => setName(e.target.value)} />      <button onClick={add}>أضف</button>      <ul>        {list.map(p => (          <li key={p.id}>            {p.name} <button onClick={() => setList(list.filter(x => x.id !== p.id))}>حذف</button>          </li>        ))}      </ul>      <p className="total">الحاضرون: {list.length}</p>    </div>  );}
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس