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

دورة حياة المكوّن: 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>  );}

لاحظ الرجوع المبكر 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>  );}

جرّب «دورة 9» غير الموجودة. والمتغير ignore يمنع مشكلة شائعة: إن ضغطت دورتين بسرعة، لا يكتب الرد القديم فوق الجديد.

توقّع وتحقّق

متى يعمل useEffect(f, []) ؟

مصفوفة فارغة.

ما وظيفة الدالة التي يعيدها التأثير؟

Cleanup.

أي مما يلي عمل جانبي مكانه useEffect؟

يتعامل مع العالم الخارجي.

التأثير useEffect(f, [id]) يُعاد تشغيله حين:

الاعتماديات.

الخلاصة

تحدٍّ برمجي: مؤقت المذاكرة

متوسط

مؤقت «بومودورو» مبسّط: يعد الثواني تنازليًّا، ويمكن إيقافه واستئنافه.

المطلوب:
  • يبدأ من 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>  );}
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

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