دورة حياة المكوّن: useEffect
متى يظهر المكوّن ومتى يختفي، وأين نجلب البيانات؟ ⏱ نحو 35 دقيقة.
- تشرح مراحل دورة حياة المكوّن وما يقابلها في
useEffect. - تستخدم مصفوفة الاعتماديات بشكل صحيح.
- تنظّف المؤقتات والاشتراكات بدالة التنظيف.
- تجلب البيانات مع حالات التحميل والنجاح والخطأ.
١دورة حياة المكوّن
كل مكوّن يمر بثلاث مراحل: يُركَّب Mount (يظهر في الصفحة أول مرة)، ثم يُحدَّث Update (يُعاد رسمه مع كل تغيّر في حالته أو خصائصه)، ثم يُزال Unmount (يختفي من الصفحة).
الرسم نفسه يجب أن يكون «نقيًّا»: يحسب JSX من البيانات فقط. أما الأعمال الجانبية Side effects، مثل جلب البيانات من الخادم، أو تشغيل مؤقت، أو تغيير عنوان التبويب، فمكانها useEffect: دالة يشغّلها React بعد ظهور المكوّن على الشاشة.
| ما تريده | useEffect | ما يقابله في مكوّنات الأصناف القديمة |
|---|---|---|
| مرة واحدة بعد الظهور | useEffect(f, []) | componentDidMount |
| بعد تغيّر قيمة معيّنة | useEffect(f, [id]) | componentDidUpdate |
| قبل الإزالة (تنظيف) | return () => {…} | componentWillUnmount |
| بعد كل رسم | useEffect(f) (نادرًا ما تحتاجه) | — |
٢أول تأثير: عنوان التبويب
import { useState, useEffect } from 'react'; export default function App() { const [count, setCount] = useState(0); useEffect(() => { document.title = `لديك ${count} رسائل`; console.log('التأثير يعمل، count =', count); }, [count]); return <button onClick={() => setCount(count + 1)}>رسالة جديدة ({count})</button>;}
اضغط الزر وراقب Console: يعمل التأثير بعد الظهور الأول، ثم كلما تغيّرت count فقط، لأنها في مصفوفة الاعتماديات [count].
٣التنظيف: المؤقتات والاشتراكات
إن بدأ التأثير شيئًا مستمرًا (مؤقت setInterval، أو اتصال)، فعليه أن يوقفه حين يُزال المكوّن، وإلا بقي يعمل في الخلفية. الدالة التي يعيدها التأثير هي دالة التنظيف:
import { useState, useEffect } from 'react'; function Clock() { const [sec, setSec] = useState(0); useEffect(() => { const id = setInterval(() => setSec(s => s + 1), 1000); console.log('▶ بدأ المؤقت'); return () => { clearInterval(id); console.log('⏹ أُوقف المؤقت'); }; }, []); return <p>⏱ مضى {sec} ثانية</p>;} export default function App() { const [show, setShow] = useState(true); return ( <div> <button onClick={() => setShow(!show)}>{show ? 'إخفاء' : 'إظهار'} المؤقت</button> {show && <Clock />} </div> );}
اضغط «إخفاء» ثم «إظهار» وراقب Console: كل إزالة توقف المؤقت، وكل إظهار يبدأ مؤقتًا جديدًا من الصفر.
٤جلب البيانات: التحميل والنجاح والخطأ
أشهر استخدام لـ useEffect: جلب البيانات حين يظهر المكوّن. والتطبيق الجيد يعرض ثلاث حالات: جارٍ التحميل، ثم البيانات، أو رسالة خطأ. هنا خادم محاكى في api.js يرد بعد نصف ثانية:
import { useState, useEffect } from 'react';import { fetchCourses } from './api.js'; export default function App() { const [courses, setCourses] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); useEffect(() => { fetchCourses() .then(data => setCourses(data)) .catch(err => setError(err.message)) .finally(() => setLoading(false)); }, []); if (loading) return <p>⏳ جارٍ التحميل…</p>; if (error) return <p style={{ color: 'crimson' }}>✗ {error}</p>; return ( <ul> {courses.map(c => <li key={c.id}>{c.title} — {c.hours} ساعات</li>)} </ul> );}
// api.js: محاكاة لخادم بعيد يرد بعد قليل (في الوحدة 7 نستبدلها بطلب حقيقي)const COURSES = [ { id: 1, title: 'أطر الويب', hours: 3 }, { id: 2, title: 'أمن الويب', hours: 3 }, { id: 3, title: 'ضمان الجودة', hours: 4 },]; export function fetchCourses() { return new Promise(resolve => setTimeout(() => resolve(COURSES), 600));} export function fetchCourse(id) { return new Promise((resolve, reject) => setTimeout(() => { const c = COURSES.find(x => x.id === id); c ? resolve(c) : reject(new Error('الدورة غير موجودة')); }, 400));}
لاحظ الرجوع المبكر if (loading) return …: لكل حالة شكل واضح. وبعد أن نبني الخادم بـ Express (الوحدة 4) نستبدل fetchCourses بـ fetch('/api/courses') دون تغيير بقية المكوّن.
٥حين يتغير ما نجلبه
إن كان الجلب يعتمد على قيمة (رقم الدورة المختارة)، فضعها في الاعتماديات، فيُعاد الجلب كلما تغيّرت. والدالة async لا توضع مباشرة في useEffect، بل نعرّفها داخله:
import { useState, useEffect } from 'react';import { fetchCourse } from './api.js'; export default function App() { const [id, setId] = useState(1); const [course, setCourse] = useState(null); const [error, setError] = useState(''); useEffect(() => { let ignore = false; // نتجاهل ردًّا قديمًا وصل متأخرًا async function load() { setError(''); setCourse(null); try { const c = await fetchCourse(id); if (!ignore) setCourse(c); } catch (e) { if (!ignore) setError(e.message); } } load(); return () => { ignore = true; }; }, [id]); return ( <div> {[1, 2, 3, 9].map(n => ( <button key={n} onClick={() => setId(n)} disabled={n === id}>دورة {n}</button> ))} {!course && !error && <p>⏳ …</p>} {course && <h3>{course.title}</h3>} {error && <p style={{ color: 'crimson' }}>{error}</p>} </div> );}
// api.js: محاكاة لخادم بعيد يرد بعد قليل (في الوحدة 7 نستبدلها بطلب حقيقي)const COURSES = [ { id: 1, title: 'أطر الويب', hours: 3 }, { id: 2, title: 'أمن الويب', hours: 3 }, { id: 3, title: 'ضمان الجودة', hours: 4 },]; export function fetchCourses() { return new Promise(resolve => setTimeout(() => resolve(COURSES), 600));} export function fetchCourse(id) { return new Promise((resolve, reject) => setTimeout(() => { const c = COURSES.find(x => x.id === id); c ? resolve(c) : reject(new Error('الدورة غير موجودة')); }, 400));}
جرّب «دورة 9» غير الموجودة. والمتغير ignore يمنع مشكلة شائعة: إن ضغطت دورتين بسرعة، لا يكتب الرد القديم فوق الجديد.
توقّع وتحقّق
متى يعمل useEffect(f, []) ؟
ما وظيفة الدالة التي يعيدها التأثير؟
Cleanup.أي مما يلي عمل جانبي مكانه useEffect؟
التأثير useEffect(f, [id]) يُعاد تشغيله حين:
الخلاصة
- الرسم نقي؛ والأعمال الجانبية في
useEffect، وتعمل بعد الظهور. []مرة واحدة، و[x]عند تغيّرx، وبلا مصفوفة بعد كل رسم.return () => clearInterval(id): التنظيف قبل الإزالة.- الجلب:
loadingثمdataأوerror، مع رجوع مبكر لكل حالة. - لا
asyncمباشرة علىuseEffect: عرّف دالة داخله واستدعها.
تحدٍّ برمجي: مؤقت المذاكرة
متوسطمؤقت «بومودورو» مبسّط: يعد الثواني تنازليًّا، ويمكن إيقافه واستئنافه.
- يبدأ من 5 ثوانٍ (لتسهيل التجربة)، ويعرض
<p className="time">5</p>. - زر «ابدأ» يشغّله (ينقص 1 كل ثانية)، ويتحول إلى «إيقاف» أثناء العمل.
- عند الصفر يتوقف وحده ويعرض
<p className="done">انتهى الوقت!</p>. - المؤقت يعمل داخل useEffect مع تنظيف clearInterval.
👀 اعرض الناتج المطلوب (وجرّبه)
- الكود يعمل بلا أخطاء في Console
- يبدأ من 5
- «ابدأ» يشغّله: بعد ثانيتين تقريبًا يصير 3 أو 4، والزر «إيقاف»
- «إيقاف» يجمّده
- يصل للصفر ويعرض «انتهى الوقت!»
- يستخدم useEffect و clearInterval
💡 تلميح 1
💡 تلميح 2
[running]: إن لم يعمل فلا تفعل شيئًا، وإلا ابدأ setInterval وأعد دالة clearInterval.حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
import { useState, useEffect } from 'react'; export default function App() { const [left, setLeft] = useState(5); const [running, setRunning] = useState(false); useEffect(() => { if (!running) return; const id = setInterval(() => { setLeft(s => { if (s <= 1) { setRunning(false); return 0; } return s - 1; }); }, 1000); return () => clearInterval(id); }, [running]); return ( <div> <p className="time">{left}</p> <button onClick={() => setRunning(!running)} disabled={left === 0}>{running ? 'إيقاف' : 'ابدأ'}</button> {left === 0 && <p className="done">انتهى الوقت!</p>} </div> );}