React

useEffect في React — دليل شامل للتأثيرات الجانبية 2026

📅 2026-10-15⏱ 8 دقائق قراءة
في المقال السابق، تعلمت الأحداث والنماذج. الآن سنتعلم **useEffect** — وهو Hook يسمح بتشغيل **تأثيرات جانبية** (Side Effects) داخل المكونات. في هذا الدليل العملي، سنأخذك خطوة بخطوة لفهم useEffect ومتى تستخدمه، مع تمارين وحلول. ## ما هو Side Effect؟ **Side Effect** (تأثير جانبي) هو أي شيء **يحدث خارج المكوّنة**: - طلب API (Fetch). - تعديل DOM مباشرة. - تعيين مؤقت (setTimeout). - الاشتراك في event. - تعديل `document.title`. **⚠️ مشكلة:** لا يمكن وضع هذه العمليات **داخل** render (سيسبب مشاكل). **✅ الحل:** استخدم `useEffect`. ## لماذا نحتاج useEffect؟ **دورة حياة المكوّنة:** ``` 1. Mount (التركيب) → useEffect بدون dependencies 2. Update (التحديث) → useEffect مع dependencies 3. Unmount (الإزالة) → useEffect مع cleanup ``` ## الصيغة الأساسية ```tsx import { useEffect } from "react"; useEffect(() => { // الكود الذي ينفذ return () => { // دالة التنظيف (اختياري) }; }, [dependencies]); ``` ## حالات useEffect ### 1. بدون dependencies — ينفذ بعد كل render ```tsx useEffect(() => { console.log("ينفذ بعد كل render"); }); ``` **⚠️ استخدم بحذر** — قد يسبب حلقات لا نهائية. ### 2. مع مصفوفة فارغة — ينفذ مرة واحدة (Mount) ```tsx useEffect(() => { console.log("ينفذ مرة واحدة فقط"); }, []); ``` **الاستخدام:** جلب بيانات أولية، إعداد مكتبة، ... ### 3. مع dependencies — ينفذ عند تغير القيم ```tsx useEffect(() => { console.log("ينفذ عندما يتغير count"); }, [count]); ``` **الاستخدام:** الاستجابة لتغير قيمة معينة. ## دالة التنظيف (Cleanup) ```tsx useEffect(() => { const timer = setInterval(() => { console.log("tick"); }, 1000); // دالة التنظيف: return () => { clearInterval(timer); }; }, []); ``` **⚠️ مهم:** دالة التنظيف: - تُنفذ عند **إزالة المكوّنة**. - أو قبل **إعادة تنفيذ useEffect**. ## أمثلة عملية ### 1. تعديل عنوان الصفحة ```tsx function PageTitle() { const [count, setCount] = useState(0); useEffect(() => { document.title = `العدد: ${count}`; }, [count]); return <button onClick={() => setCount(count + 1)}>+1</button>; } ``` **كل نقرة تُحدّث عنوان الصفحة!** ### 2. المؤقت (Timer) ```tsx function Timer() { const [seconds, setSeconds] = useState(0); useEffect(() => { const interval = setInterval(() => { setSeconds((s) => s + 1); }, 1000); return () => clearInterval(interval); }, []); return <p>مرّ {seconds} ثانية</p>; } ``` ### 3. جلب بيانات من API ```tsx function Users() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); useEffect(() => { fetch("https://jsonplaceholder.typicode.com/users") .then((res) => res.json()) .then((data) => { setUsers(data); setLoading(false); }) .catch((err) => { setError(err.message); setLoading(false); }); }, []); if (loading) return <p>جاري التحميل...</p>; if (error) return <p>خطأ: {error}</p>; return ( <ul> {users.map((user: any) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } ``` ### 4. الاعتماد على متغير ```tsx function SearchUsers() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); useEffect(() => { if (query === "") { setResults([]); return; } const timer = setTimeout(() => { fetch(`https://api.example.com/search?q=${query}`) .then((res) => res.json()) .then(setResults); }, 500); return () => clearTimeout(timer); }, [query]); return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <ul>{results.map((r: any) => <li key={r.id}>{r.name}</li>)}</ul> </div> ); } ``` **الفكرة:** Debouncing — لا يرسل الطلب إلا بعد 500 مللي ثانية من التوقف عن الكتابة. ### 5. الاشتراك في event ```tsx function WindowSize() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); return <p>عرض الشاشة: {width}px</p>; } ``` ## useEffect مع TypeScript ### 1. جلب بيانات ```tsx interface User { id: number; name: string; email: string; } function Users() { const [users, setUsers] = useState<User[]>([]); useEffect(() => { async function fetchUsers() { const res = await fetch("https://jsonplaceholder.typicode.com/users"); const data: User[] = await res.json(); setUsers(data); } fetchUsers(); }, []); return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>; } ``` ### 2. دالة التنظيف ```tsx useEffect(() => { const timer: NodeJS.Timeout = setTimeout(() => { console.log("بعد ثانية"); }, 1000); return () => clearTimeout(timer); }, []); ``` ## useEffect vs useLayoutEffect <table> <thead> <tr> <th>المعيار</th> <th>useEffect</th> <th>useLayoutEffect</th> </tr> </thead> <tbody> <tr> <td><strong>التوقيت</strong></td> <td>بعد الرسم</td> <td>قبل الرسم</td> </tr> <tr> <td><strong>الأداء</strong></td> <td>✅ أفضل</td> <td>⚠️ يحجب الرسم</td> </tr> <tr> <td><strong>الاستخدام</strong></td> <td>معظم الحالات</td> <td>قياس DOM</td> </tr> </tbody> </table> **القاعدة:** استخدم `useEffect` دائماً، إلا إذا كنت تقيس DOM. ## أخطاء شائعة ### 🔴 الخطأ 1: حلقة لا نهائية ```tsx // ❌ خطأ: useEffect(() => { setCount(count + 1); // تحديث state ← إعادة render ← useEffect ← ... }, [count]); ``` **الحل:** لا تحدّث state الذي تعتمد عليه. ### 🔴 الخطأ 2: نسيان dependencies ```tsx // ❌ خطأ: لا يتحدث عند تغير count useEffect(() => { document.title = `العدد: ${count}`; }, []); // ← count مفقود // ✅ صحيح: useEffect(() => { document.title = `العدد: ${count}`; }, [count]); ``` ### 🔴 الخطأ 3: نسيان دالة التنظيف ```tsx // ❌ خطأ: تسريب ذاكرة useEffect(() => { const timer = setInterval(() => {}, 1000); // لا يوجد cleanup }, []); // ✅ صحيح: useEffect(() => { const timer = setInterval(() => {}, 1000); return () => clearInterval(timer); }, []); ``` ### 🔴 الخطأ 4: استخدام async مباشرة ```tsx // ❌ خطأ: useEffect(async () => { const data = await fetch("/api"); }, []); // ✅ صحيح: useEffect(() => { async function fetchData() { const data = await fetch("/api"); } fetchData(); }, []); ``` ## تمارين عملية ### تمرين 1: عنوان ديناميكي أنشئ مكوّنة تحدّث عنوان الصفحة بعدّاد. **الحل:** ```tsx function TitleUpdater() { const [count, setCount] = useState(0); useEffect(() => { document.title = `العدد: ${count}`; }, [count]); return <button onClick={() => setCount(count + 1)}>{count}</button>; } ``` ### تمرين 2: مؤقت تنازلي أنشئ مؤقتاً يبدأ من 10 وينتهي عند 0. **الحل:** ```tsx function Countdown() { const [count, setCount] = useState(10); useEffect(() => { if (count === 0) return; const timer = setTimeout(() => setCount(count - 1), 1000); return () => clearTimeout(timer); }, [count]); return <p>{count === 0 ? "⏰ انتهى!" : count}</p>; } ``` ### تمرين 3: جلب مستخدمين اجلب قائمة مستخدمين من API واعرضها. **الحل:** ```tsx function UsersList() { const [users, setUsers] = useState<any[]>([]); useEffect(() => { fetch("https://jsonplaceholder.typicode.com/users") .then((res) => res.json()) .then(setUsers); }, []); return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>; } ``` ### تمرين 4: عرض حجم الشاشة اعرض عرض الشاشة وحدّثه عند تغيير الحجم. **الحل:** ```tsx function WindowSize() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handler = () => setWidth(window.innerWidth); window.addEventListener("resize", handler); return () => window.removeEventListener("resize", handler); }, []); return <p>العرض: {width}px</p>; } ``` ### تمرين 5: العدّاد التلقائي أنشئ عدّاداً يزيد تلقائياً كل ثانية. **الحل:** ```tsx function AutoCounter() { const [count, setCount] = useState(0); useEffect(() => { const interval = setInterval(() => setCount((c) => c + 1), 1000); return () => clearInterval(interval); }, []); return <p>{count}</p>; } ``` ### تمرين 6: البحث مع Debounce أنشئ حقل بحث لا يرسل الطلب إلا بعد 500ms. **الحل:** ```tsx function DebouncedSearch() { const [query, setQuery] = useState(""); const [results, setResults] = useState<string[]>([]); useEffect(() => { if (!query) { setResults([]); return; } const timer = setTimeout(() => { setResults([`نتيجة 1 لـ "${query}"`, `نتيجة 2 لـ "${query}"`]); }, 500); return () => clearTimeout(timer); }, [query]); return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <ul>{results.map((r, i) => <li key={i}>{r}</li>)}</ul> </div> ); } ``` ### تمرين 7: تسجيل الدخول التلقائي أنشئ مكوّنة تسجّل رسالة عند mount و unmount. **الحل:** ```tsx function Logger() { useEffect(() => { console.log("🟢 تم التركيب"); return () => { console.log("🔴 تم الإزالة"); }; }, []); return <p>افتح Console</p>; } ``` ### تمرين 8: مراقبة الشبكة أضف مستمعاً لحدث `online` و `offline`. **الحل:** ```tsx function NetworkStatus() { const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { const online = () => setIsOnline(true); const offline = () => setIsOnline(false); window.addEventListener("online", online); window.addEventListener("offline", offline); return () => { window.removeEventListener("online", online); window.removeEventListener("offline", offline); }; }, []); return <p>{isOnline ? "🟢 متصل" : "🔴 غير متصل"}</p>; } ``` ## حل المشاكل الشائعة ### 🔴 المشكلة 1: حلقة لا نهائية **السبب:** تحديث state داخل useEffect مع dependency عليه. **الحل:** احذف dependency أو لا تحدّث state. ### 🔴 المشكلة 2: البيانات لا تُحدّث **السبب:** dependency فارغ `[]`. **الحل:** أضف المتغيرات التي يعتمد عليها الكود. ### 🔴 المشكلة 3: تسريب ذاكرة **السبب:** عدم تنظيف `setInterval`/`setTimeout`. **الحل:** استخدم دالة التنظيف. ### 🔴 المشكلة 4: `Warning: Can't perform a React state update on an unmounted component` **السبب:** تحديث state بعد إزالة المكوّنة. **الحل:** استخدم flag: ```tsx useEffect(() => { let isMounted = true; fetch("/api").then((data) => { if (isMounted) setData(data); }); return () => { isMounted = false; }; }, []); ``` ## جدول useEffect <table> <thead> <tr> <th>الصيغة</th> <th>متى ينفذ؟</th> <th>الاستخدام</th> </tr> </thead> <tbody> <tr> <td><code>useEffect(() =&gt; {})</code></td> <td>بعد كل render</td> <td>⚠️ نادراً</td> </tr> <tr> <td><code>useEffect(() =&gt; {}, [])</code></td> <td>مرة واحدة (Mount)</td> <td>✅ جلب بيانات</td> </tr> <tr> <td><code>useEffect(() =&gt; {}, [dep])</code></td> <td>عند تغير dep</td> <td>✅ الاستجابة</td> </tr> <tr> <td><code>return () =&gt; {}</code></td> <td>عند Unmount</td> <td>✅ التنظيف</td> </tr> </tbody> </table> ## قائمة تحقق نهائية <table> <thead> <tr> <th>المهمة</th> <th>الحالة</th> </tr> </thead> <tbody> <tr> <td>فهم Side Effects</td> <td>⬜</td> </tr> <tr> <td>useEffect مع dependency فارغ</td> <td>⬜</td> </tr> <tr> <td>useEffect مع dependencies</td> <td>⬜</td> </tr> <tr> <td>دالة التنظيف (Cleanup)</td> <td>⬜</td> </tr> <tr> <td>جلب بيانات من API</td> <td>⬜</td> </tr> <tr> <td>Debouncing</td> <td>⬜</td> </tr> <tr> <td>حل التمارين الثمانية</td> <td>⬜</td> </tr> </tbody> </table> ## ماذا بعد هذا المقال؟ الآن بعد أن أتقنت useEffect، أنت جاهز للمقال التالي: 1. **القوائم والشروط** — العرض الديناميكي. 2. **React Router** — التنقل بين الصفحات. 3. **Context API** — إدارة الحالة العامة. ## الخلاصة في هذا المقال، تعلمت: - ✅ ما هو Side Effect ولماذا نحتاجه. - ✅ الصيغة الأساسية لـ useEffect. - ✅ الحالات الثلاث (Mount, Update, Unmount). - ✅ دالة التنظيف (Cleanup). - ✅ جلب بيانات من API. - ✅ Debouncing. - ✅ حل 8 تمارين عملية. **تذكر:** useEffect قوي لكن **لا تبالغ في استخدامه**. أحياناً هناك حلول أفضل. **هل أتقنت useEffect؟ شاركنا في التعليقات!**