الوحدة 3 · الـ State والأحداث
أساسيات useEffect
19 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
استخدم useEffect لإبقاء الـ state في React متزامنًا مع أنظمة خارجها، واكتب الاعتماديات ودوال التنظيف بشكل صحيح، وتعرّف على الحالات الكثيرة التي لا تحتاج فيها إلى effect أصلًا.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
حتى الآن، كان كل ما في الـ components متعلّقًا بـ React: props تدخل، وJSX يخرج، وتغييرات الـ state تُطلق الـ render. لكن Watchlist يحتاج أيضًا إلى لمس أشياء لا تديرها React. يجب أن يقول تبويب المتصفّح «2 to watch»، ويجب أن تنجو القائمة من تحديث الصفحة بحفظها في localStorage. لا شيء من هذا رسم. إنه تزامن (synchronization): إبقاء شيء خارج React متوافقًا مع الـ state الخاص بك. وهذه وظيفة الـ effects.
أول effect لك
import { useEffect, useState } from 'react';
export default function App() {
const [movies, setMovies] = useState(INITIAL_MOVIES);
const left = movies.filter((m) => !m.watched).length;
useEffect(() => {
document.title = `${left} to watch · Watchlist`;
}, [left]);
// …render the app
}تأخذ useEffect دالة ومصفوفة اعتماديات (dependency array). ترسم React الـ component، وتحدّث الشاشة، ثم تشغّل الدالة. وبعد مرّات الـ render اللاحقة، لا تشغّلها مجددًا إلا إذا تغيّر شيء في مصفوفة الاعتماديات منذ المرة السابقة. علّم فيلمًا كمُشاهَد، فتنتقل left من 2 إلى 1، ويتحدّث العنوان. اكتب في نموذج الإضافة، فلا تتغيّر left، ويُتخطّى الـ effect.
لمصفوفة الاعتماديات ثلاثة أشكال:
| ما تكتبه | متى يعمل الـ effect |
|---|---|
useEffect(fn, [a, b]) |
بعد الـ render الأول، وكلما تغيّرت a أو b |
useEffect(fn, []) |
بعد الـ render الأول فقط |
useEffect(fn) |
بعد كل render |
وقاعدة ما يوضع في المصفوفة ليست خيارًا: اسرد كل قيمة من الـ component يقرؤها الـ effect، مثل الـ props والـ state والمتغيّرات المشتقّة منهما. وقاعدة الـ lint الخاصة بالاعتماديات في مشروع Vite (react/exhaustive-deps في Oxlint، وreact-hooks/exhaustive-deps في ESLint) تتحقّق من ذلك نيابةً عنك. إذا أغفلت شيئًا، فسيظل الـ effect يستخدم قيمة قديمة، وهذا هو الخطأ في سؤال الاختبار الثالث.
التنظيف
بعض الـ effects تبدأ شيئًا يجب إيقافه: مؤقّتًا، أو مستمع أحداث، أو اتصالًا. أرجِع دالة تلغيه:
useEffect(() => {
function handleKeyDown(e) {
if (e.key === '/') focusSearch();
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);تستدعي React دالة التنظيف قبل تشغيل الـ effect مجددًا، وحين يُزال الـ component من الشاشة. ومن دونها، سيضيف كل تركيب مستمعًا آخر، وسيعمل الضغط على «/» عدّة مرات.
لماذا يعمل مرتين في وضع التطوير
مع <StrictMode> (الموجود في main.jsx لديك)، تفعل React شيئًا غريبًا أثناء التطوير: تركّب الـ component، وتشغّل الـ effects، ثم تشغّل دوال تنظيفها فورًا كأنها تلغي التركيب، ثم تشغّل الـ effects مجددًا. إنه اختبار ضغط. إذا كانت دالة التنظيف تلغي الـ effect بشكل صحيح، فسيتصرّف التطبيق بالطريقة نفسها، ولن ترى إلا سطرًا إضافيًا في السجل. وإن لم تكن كذلك، كأن تنسى إزالة مستمع، فستلاحظ الآن بدل أن تلاحظ في الإنتاج. لا تحاول منع التشغيل المزدوج؛ بل أصلح دالة التنظيف.
حفظ Watchlist في localStorage
حفظ الـ state هو الـ effect النموذجي: كلما تغيّرت movies، اكتبها في التخزين.
const STORAGE_KEY = 'watchlist.movies';
function loadMovies() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) ?? [];
} catch {
return [];
}
}
export default function App() {
const [movies, setMovies] = useState(loadMovies);
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(movies));
}, [movies]);
// …
}هناك تفصيلتان مهمّتان. التحميل يحدث في دالة التهيئة (initializer)، useState(loadMovies)، لا في effect. تمرير الدالة (لا استدعاؤها) يعني أن React تقرأ التخزين للـ render الأول فقط، لا في كل render، فتظهر القائمة المحفوظة فورًا. أمّا التحميل في effect فسيرسم قائمة فارغة أولًا، وقد يكتب effect الحفظ تلك المصفوفة الفارغة فوق أفلامك المخزّنة. كما تستخدم loadMovies كتلة try/catch، لأن التخزين قد يكون محظورًا أو يحتوي على بيانات تالفة، والقيمة المعطوبة يجب ألّا تُسقط التطبيق كله.
قد لا تحتاج إلى effect
الـ effects مخرج طوارئ، وأكثر أخطائها شيوعًا تأتي من استخدامها حيث لا حاجة إليها. قبل أن تكتب واحدًا، افحص هذه الحالات الثلاث.
حساب شيء من الـ props أو الـ state. القوائم المصفّاة والأعداد والنصوص المنسّقة تُحسب أثناء الـ render. تخزينها في الـ state ومزامنتها بـ effect يضيف render إضافيًا ببيانات قديمة، ومزيدًا من الـ state الذي قد ينحرف.
الاستجابة لإجراء من المستخدم. إذا كان يجب أن يُنفَّذ الكود لأن المستخدم نقر Add، فضعه في معالج الإرسال. الـ effect الذي يراقب movies لا يستطيع التمييز بين الإضافة والحذف وإعادة التحميل.
إعادة ضبط الـ state حين يتغيّر prop. بدل effect يمسح مسوّدة حين تتغيّر movieId، امنح الـ component قيمة key: <NotesEditor key={movieId} />. الـ key الجديد يعني component جديدًا بـ state جديد.
وما يتبقّى هو التزامن الحقيقي: واجهات المتصفّح، والتخزين، والمؤقّتات، والاشتراكات، واتصالات الشبكة. قائمة قصيرة، وهي القائمة الصحيحة.
بهذا يكتمل جوهر React: الـ components والـ props والـ state والأحداث والـ effects. في القسم التالي تجمع كل القطع في Watchlist المكتمل، وتنسّقه، وتنشره.
أهم النقاط
- يعمل الـ effect بعد أن تحدّث React الشاشة، ووظيفته التزامن مع شيء خارج React، مثل عنوان المستند أو localStorage أو المؤقّتات أو الاشتراكات.
- تسرد مصفوفة الاعتماديات كل قيمة من الـ component يقرؤها الـ effect؛ وتعيد React تشغيل الـ effect حين تتغيّر إحداها.
- أرجِع دالة تنظيف (cleanup) تلغي ما فعله الـ effect؛ تشغّلها React قبل التشغيل التالي وعند إلغاء التركيب.
- في وضع التطوير، يشغّل StrictMode كل effect، ثم دالة تنظيفه، ثم الـ effect من جديد، ليثبت أن التنظيف يعمل.
- إذا كان يمكن حساب القيمة من الـ props أو الـ state، أو كان التغيير ناتجًا عن إجراء محدّد من المستخدم، فلا تحتاج إلى effect.