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

الوحدة 3 · الـ State والأحداث

أساسيات useState

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

الدرس في لمحة

امنح الـ components ذاكرة باستخدام useState، وافهم الـ state كلقطة خاصة بكل render، واستخدم دوال التحديث للتحديثات المتتالية، واستبدل المصفوفات والكائنات بدل تعديلها.

  1. 1الـ useState
  2. 2كل render لقطة
  3. 3المصفوفات والكائنات: استبدل، لا تعدّل
  4. 4اختيار القيمة الابتدائية

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

انقر زر «Mark as watched» في function component عادي، ولن يحدث شيء. هذا هو الكود الذي يكتبه معظم الناس أول مرة:

JSX
export default function MovieItem() {
  let watched = false;

  function handleClick() {
    watched = !watched;
  }

  return (
    <button onClick={handleClick}>
      {watched ? 'Watched' : 'Mark as watched'}
    </button>
  );
}

هناك خطآن. تغيير متغيّر محلي لا يُخبر React بأن شيئًا حدث، فلا تعيد الـ render أبدًا. وحتى لو تسبّب شيء آخر في render، فسيُنفَّذ let watched = false مرة أخرى ويعيد القيمة إلى البداية، لأن الـ component دالة، ومتغيّراتها المحلية تبدأ من جديد مع كل استدعاء. يحتاج الـ component إلى شيئين: ذاكرة تبقى بين مرّات الـ render، وطريقة ليطلب من React أن تعيد الـ render. هذا هو الـ state.

الـ useState

src/components/MovieItem.jsx
import { useState } from 'react';

export default function MovieItem({ title }) {
  const [watched, setWatched] = useState(false);

  return (
    <li>
      {title}
      <button aria-pressed={watched} onClick={() => setWatched(!watched)}>
        {watched ? 'Watched' : 'Mark as watched'}
      </button>
    </li>
  );
}

تُعلن useState(false) عن قطعة state واحدة بقيمة ابتدائية false. وتُرجع زوجًا تفكّكه وتسمّيه: القيمة الحالية، ودالة setter (دالة الضبط). التسمية [thing, setThing] عُرف يتّبعه الجميع؛ التزم به.

حين تستدعي setWatched(true)، تخزّن React القيمة الجديدة وتجدول re-render. فتستدعي MovieItem مرة أخرى، وهذه المرة تُرجع useState القيمة true. يُحسب الـ JSX الخاص بك منها، وتحدّث React الـ DOM ليطابقه.

الـ state ملك لـ نسخة (instance) من الـ component، لا للدالة. ارسم <MovieItem /> ثلاث مرات وستحصل على ثلاث قيم watched مستقلة. النقر على واحدة لا يؤثّر في الأخريين.

كل render لقطة

هذه هي الفكرة التي تجعل الـ state مفهومًا فجأة. حين تستدعي React الـ component، تسلّمك الـ state الخاص بذلك الـ render. وكل ما في ذلك الـ render، الـ JSX ومعالجات الأحداث وكل شيء، يرى تلك القيم. ضبط الـ state لا يغيّر المتغيّر الذي لديك أصلًا؛ بل يطلب render جديدًا بقيمة جديدة.

الـ render الأول يرى watched تساوي false. نقرة تستدعي setWatched(true)، التي تجدول الـ render الثاني. الـ render الثاني يرى watched تساوي true، وتثبّت React الـ DOM المتغيّر. Render 1 watched = false نقرة setWatched(true) Render 2 watched = true المستخدم يتصرّف React تعيد الـ render لكل render قيمه الخاصة؛ لا شيء يتغيّر في منتصفه
ضبط الـ state يُطلق render جديدًا بلقطة جديدة

ولهذا يُفاجأ الناس بهذا:

JSX
const [count, setCount] = useState(0);

function addThree() {
  setCount(count + 1); // count is 0 → asks for 1
  setCount(count + 1); // count is still 0 → asks for 1
  setCount(count + 1); // still 0 → 1
}

الاستدعاءات الثلاثة تقرأ اللقطة نفسها، فتكون النتيجة 1 لا 3. وحين تعتمد القيمة التالية على السابقة، مرّر دالة تحديث (updater function) بدلًا من قيمة:

JSX
function addThree() {
  setCount((c) => c + 1); // 0 → 1
  setCount((c) => c + 1); // 1 → 2
  setCount((c) => c + 1); // 2 → 3
}

تضع React دوال التحديث في طابور وتنفّذها بالترتيب، وكل واحدة تستقبل نتيجة سابقتها. لست بحاجة إلى دوال التحديث في كل مكان؛ فـ setWatched(!watched) في معالج نقرة لا بأس به. استخدمها حين تضبط الـ state نفسه أكثر من مرة في معالج واحد، أو في كود يُنفَّذ لاحقًا، مثل مؤقّت.

المصفوفات والكائنات: استبدل، لا تعدّل

يحتفظ Watchlist بمصفوفة من كائنات الأفلام في الـ state. وتقرّر React ما إذا كان الـ state قد تغيّر بمقارنة القيمة القديمة بالجديدة باستخدام Object.is. فإذا غيّرت مصفوفة في مكانها وأعدت المصفوفة نفسها، ترى React المرجع نفسه وقد تتخطّى الـ render كليًا.

لذا تعامل مع الـ state كقيمة للقراءة فقط، وابنِ قيمًا جديدة:

JSX
const [movies, setMovies] = useState(INITIAL_MOVIES);

// Add: a new array with everything plus one more
setMovies([...movies, { id: crypto.randomUUID(), title: 'Dune', watched: false }]);

// Remove: a new array without one item
setMovies(movies.filter((m) => m.id !== id));

// Update one: a new array, with a new object for the changed item
setMovies(movies.map((m) => (m.id === id ? { ...m, watched: !m.watched } : m)));

هذه الأسطر الثلاثة (الـ spread للإضافة، وfilter للحذف، وmap مع spread للتحديث) تغطّي معظم تحديثات الـ state التي ستكتبها للقوائم. والكائنات تعمل بالطريقة نفسها: setFilters({ ...filters, year: 2024 }).

اختيار القيمة الابتدائية

الوسيط الممرَّر إلى useState لا يُستخدم إلا في الـ render الأول، وتتجاهله مرّات الـ render اللاحقة. وإذا كان حساب القيمة الابتدائية مكلفًا، كقراءة بيانات محفوظة وتحليلها، فمرّر دالة بدلًا منها، useState(() => loadMovies())، فتستدعيها React في الـ render الأول فقط بدل أن تستدعيها في كل render. وستستخدم هذا بالضبط لتحميل Watchlist من localStorage في القسم 4.

صار بإمكان الـ components أن تتذكّر. في الدرس التالي تنظر عن قرب إلى الأحداث التي تغيّر الـ state: النقر، ولوحة المفاتيح، وكائن الحدث نفسه.

أهم النقاط

  • تُرجع useState القيمة الحالية ودالة setter؛ واستدعاء الـ setter يجدول re-render بالقيمة الجديدة.
  • كل render يرى لقطة ثابتة من الـ state، لذا فقراءة الـ state مباشرة بعد ضبطه تعطي القيمة القديمة.
  • استخدم دالة تحديث، setCount((c) => c + 1)، حين تعتمد القيمة التالية على السابقة.
  • لا تعدّل الـ state أبدًا؛ أنشئ مصفوفات وكائنات جديدة باستخدام الـ spread وmap وfilter حتى تلاحظ React التغيير.
  • استدعِ الـ hooks في المستوى الأعلى من الـ component، ولا تستدعها أبدًا داخل شروط أو حلقات أو دوال متداخلة.

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

تبدأ count من 0. ماذا يعرض الزر بعد نقرة واحدة؟

JSX
function handleClick() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}
السؤال 2 من 4

لماذا لا يعمل زر التبديل هذا؟ داخل الـ component يوجد let watched = false;، ومعالج الزر ينفّذ watched = !watched;.

السؤال 3 من 4

أيّ سطر يعلّم فيلمًا واحدًا كمُشاهَد في الـ state المسمّى movies بشكل صحيح؟

السؤال 4 من 4

داخل معالج نقرة تستدعي setTitle('Dune') ثم console.log(title) في السطر التالي. كان العنوان القديم ‘Arrival’. ماذا يُطبع؟

تدريب

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

زر المشاهدة والتقييم

في بطاقة الفيلم هذه ميزتان معطّلتان.

  1. زر Mark as watched (#toggle) لا يفعل شيئًا. اجعله يبدّل بين Mark as watched وWatched، مع أن تتبعه قيمة aria-pressed.
  2. التقييم: الزر +2 (#plus-two) لا يضيف إلا 1، ويمكن للتقييم أن يتجاوز 5. اجعل +2 يضيف اثنين، ولا تدع التقييم يتجاوز 5 أبدًا، أيًّا كان الزر الذي يُنقر.
import { useState } from 'react';

export default function App() {
  let watched = false;
  const [rating, setRating] = useState(0);

  function toggleWatched() {
    watched = !watched;
  }

  function addOne() {
    setRating(rating + 1);
  }

  function addTwo() {
    setRating(rating + 1);
    setRating(rating + 1);
  }

  return (
    <article>
      <h2>Arrival</h2>
      <button id="toggle" aria-pressed={watched} onClick={toggleWatched}>
        {watched ? 'Watched' : 'Mark as watched'}
      </button>
      <p id="rating">Rating: {rating}/5</p>
      <button id="plus" onClick={addOne}>+1</button>
      <button id="plus-two" onClick={addTwo}>+2</button>
    </article>
  );
}
جارٍ تحميل المحرّر…

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

المعاينة

الـ console

(لا مخرجات)

الفحوص

  • النقر على زر التبديل يعرض “Watched” ويضبط aria-pressed على true
  • النقر مجددًا يعرض “Mark as watched” وaria-pressed قيمتها false
  • الزر +2 يرفع التقييم من 0 إلى 2
  • يتوقّف التقييم عند 5