الوحدة 3 · الـ State والأحداث
أساسيات useState
20 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
امنح الـ components ذاكرة باستخدام useState، وافهم الـ state كلقطة خاصة بكل render، واستخدم دوال التحديث للتحديثات المتتالية، واستبدل المصفوفات والكائنات بدل تعديلها.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
انقر زر «Mark as watched» في function component عادي، ولن يحدث شيء. هذا هو الكود الذي يكتبه معظم الناس أول مرة:
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
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 جديدًا بقيمة جديدة.
ولهذا يُفاجأ الناس بهذا:
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) بدلًا من قيمة:
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 كقيمة للقراءة فقط، وابنِ قيمًا جديدة:
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، ولا تستدعها أبدًا داخل شروط أو حلقات أو دوال متداخلة.