useEffect Hook - إدارة التأثيرات الجانبية
فهم التأثيرات الجانبية في React
التأثيرات الجانبية هي عمليات تؤثر على شيء خارج نطاق دالة المكون التي يتم تنفيذها. تشمل الأمثلة الشائعة جلب البيانات، الاشتراكات، المؤقتات، تغيير DOM يدويًا، والسجلات.
يتيح لك useEffect hook تنفيذ التأثيرات الجانبية في مكونات الدوال. يؤدي نفس الغرض مثل componentDidMount و componentDidUpdate و componentWillUnmount في مكونات الكلاس، لكن موحدة في واجهة برمجية واحدة.
لماذا تحتاج التأثيرات الجانبية معالجة خاصة
يجب أن تكون مكونات React دوال نقية - بنفس الخصائص والحالة، يجب أن تعرض نفس واجهة المستخدم. التأثيرات الجانبية تكسر هذه النقاء، لذلك توفر React useEffect للتعامل معها بشكل يمكن التنبؤ به.
صيغة useEffect الأساسية
يأخذ useEffect hook معاملين: دالة تحتوي على كود التأثير الجانبي، ومصفوفة اعتماديات اختيارية.
import React, { useState, useEffect } from 'react';
function DocumentTitleUpdater() {
const [count, setCount] = useState(0);
// يتم تنفيذ التأثير بعد كل عرض
useEffect(() => {
document.title = `Count: ${count}`;
console.log('Effect executed');
});
return (
<div>
<h2>العدد: {count}</h2>
<button onClick={() => setCount(count + 1)}>
زيادة
</button>
</div>
);
}
export default DocumentTitleUpdater;
في هذا المثال، يتم تشغيل التأثير بعد كل عرض، مع تحديث عنوان المستند ليعكس العدد الحالي.
مصفوفة الاعتماديات
المعامل الثاني لـ useEffect هو مصفوفة اعتماديات تتحكم في وقت تشغيل التأثير:
- بدون مصفوفة اعتماديات: يتم تشغيل التأثير بعد كل عرض
- مصفوفة فارغة []: يتم تشغيل التأثير مرة واحدة فقط بعد العرض الأولي
- مصفوفة بقيم: يتم تشغيل التأثير فقط عندما تتغير تلك القيم
import React, { useState, useEffect } from 'react';
function DependencyDemo() {
const [count, setCount] = useState(0);
const [name, setName] = useState('محمد');
// يعمل فقط عند التحميل (مرة واحدة)
useEffect(() => {
console.log('تم تحميل المكون');
}, []);
// يعمل عندما يتغير العدد
useEffect(() => {
console.log(`تغير العدد إلى: ${count}`);
}, [count]);
// يعمل عندما يتغير الاسم
useEffect(() => {
console.log(`تغير الاسم إلى: ${name}`);
}, [name]);
// يعمل عندما يتغير العدد أو الاسم
useEffect(() => {
console.log(`العدد: ${count}, الاسم: ${name}`);
}, [count, name]);
return (
<div>
<h2>العدد: {count}</h2>
<button onClick={() => setCount(count + 1)}>
زيادة
</button>
<h2>الاسم: {name}</h2>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
export default DependencyDemo;
خطأ شائع: اعتماديات مفقودة
قم دائمًا بتضمين جميع القيم من نطاق المكون المستخدمة داخل التأثير في مصفوفة الاعتماديات. حذف الاعتماديات يمكن أن يؤدي إلى بيانات قديمة وأخطاء. استخدم إضافة ESLint eslint-plugin-react-hooks للكشف عن هذه المشاكل.
دوال التنظيف
يمكن للتأثيرات اختياريًا إرجاع دالة تنظيف ستقوم React بتشغيلها قبل إلغاء تحميل المكون أو قبل تشغيل التأثير مرة أخرى. هذا ضروري لمنع تسرب الذاكرة.
import React, { useState, useEffect } from 'react';
function TimerComponent() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(true);
useEffect(() => {
if (!isRunning) return;
// الإعداد: إنشاء فاصل زمني
const intervalId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// التنظيف: مسح الفاصل الزمني عند إلغاء تحميل المكون
// أو عندما تتغير الاعتماديات
return () => {
console.log('تنظيف الفاصل الزمني');
clearInterval(intervalId);
};
}, [isRunning]); // إعادة تشغيل التأثير عندما يتغير isRunning
return (
<div>
<h2>الثواني المنقضية: {seconds}</h2>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? 'إيقاف مؤقت' : 'استئناف'}
</button>
</div>
);
}
export default TimerComponent;
متى تستخدم التنظيف
استخدم دوال التنظيف لـ: الاشتراكات، المؤقتات (setTimeout/setInterval)، مستمعي الأحداث، اتصالات WebSocket، وأي موارد أخرى تحتاج إلى تنظيف يدوي.
جلب البيانات باستخدام useEffect
واحدة من أكثر حالات الاستخدام شيوعًا لـ useEffect هي جلب البيانات من API عندما يتم تحميل المكون أو عندما تتغير قيم معينة.
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// إعادة تعيين الحالة عندما يتغير userId
setLoading(true);
setError(null);
// جلب بيانات المستخدم
fetch(`https://api.example.com/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error('فشل في جلب المستخدم');
}
return response.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]); // إعادة الجلب عندما يتغير userId
if (loading) return <div>جاري التحميل...</div>;
if (error) return <div>خطأ: {error}</div>;
if (!user) return <div>لم يتم العثور على مستخدم</div>;
return (
<div>
<h2>{user.name}</h2>
<p>البريد الإلكتروني: {user.email}</p>
<p>اسم المستخدم: {user.username}</p>
</div>
);
}
export default UserProfile;
الدوال غير المتزامنة في useEffect
لا يمكنك جعل callback التأثير نفسه async، لكن يمكنك تعريف واستدعاء دالة async بداخله:
import React, { useState, useEffect } from 'react';
function PostsList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// تعريف دالة async داخل التأثير
const fetchPosts = async () => {
try {
setLoading(true);
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
setPosts(data.slice(0, 5)); // الحصول على أول 5 منشورات
} catch (error) {
console.error('خطأ في جلب المنشورات:', error);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []); // مصفوفة فارغة - جلب مرة واحدة عند التحميل
if (loading) {
return <div>جاري تحميل المنشورات...</div>;
}
return (
<div>
<h2>المنشورات الأخيرة</h2>
<ul>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body.substring(0, 100)}...</p>
</li>
))}
</ul>
</div>
);
}
export default PostsList;
أنماط useEffect الشائعة
1. مستمعي الأحداث
function WindowSize() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// التنظيف: إزالة مستمع الحدث
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // بدون اعتماديات - الإعداد مرة واحدة
return <div>عرض النافذة: {windowWidth}px</div>;
}
2. تأخير إدخال البحث
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
// التأخير: الانتظار 500 مللي ثانية بعد توقف المستخدم عن الكتابة
const timerId = setTimeout(() => {
if (searchTerm) {
fetch(`https://api.example.com/search?q=${searchTerm}`)
.then(res => res.json())
.then(data => setResults(data));
} else {
setResults([]);
}
}, 500);
// التنظيف: إلغاء المؤقت السابق
return () => clearTimeout(timerId);
}, [searchTerm]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="بحث..."
/>
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
3. مزامنة التخزين المحلي
function ThemeSelector() {
const [theme, setTheme] = useState(() => {
// التهيئة من localStorage
return localStorage.getItem('theme') || 'light';
});
useEffect(() => {
// المزامنة إلى localStorage كلما تغير المظهر
localStorage.setItem('theme', theme);
document.body.className = theme;
}, [theme]);
return (
<div>
<button onClick={() => setTheme('light')}>فاتح</button>
<button onClick={() => setTheme('dark')}>داكن</button>
<p>المظهر الحالي: {theme}</p>
</div>
);
}
تمرين 1: ساعة مباشرة
أنشئ مكون LiveClock يعرض الوقت الحالي ويتحدث كل ثانية. يجب أن تتوقف الساعة عن التحديث عند إلغاء تحميل المكون.
// المتطلبات:
// 1. عرض الوقت بتنسيق HH:MM:SS
// 2. التحديث كل ثانية باستخدام setInterval
// 3. تنظيف الفاصل الزمني عند إلغاء التحميل
// 4. إضافة زر "إيقاف" لإيقاف التحديثات مؤقتًا
تمرين 2: البحث عن المستخدمين
قم ببناء مكون UserSearch يجلب المستخدمين من https://jsonplaceholder.typicode.com/users ويقوم بتصفيتهم بناءً على إدخال البحث.
// المتطلبات:
// 1. جلب المستخدمين عند تحميل المكون
// 2. تصفية المستخدمين حسب الاسم أثناء كتابة المستخدم
// 3. تأخير التصفية لمدة 300 مللي ثانية
// 4. عرض حالة التحميل أثناء الجلب
تمرين 3: متتبع الماوس
أنشئ مكون MouseTracker يعرض موضع الماوس الحالي (إحداثيات x و y) ويتحدث مع حركة الماوس.
// المتطلبات:
// 1. تتبع أحداث mousemove على النافذة
// 2. عرض إحداثيات X و Y
// 3. تنظيف مستمع الحدث عند إلغاء التحميل
// 4. تقليل التحديثات إلى كل 100 مللي ثانية للأداء
الملخص
useEffectيتعامل مع التأثيرات الجانبية في مكونات الدوال- مصفوفة الاعتماديات تتحكم في وقت تشغيل التأثيرات
- إرجاع دوال التنظيف لمنع تسرب الذاكرة
- الأنماط الشائعة: جلب البيانات، الاشتراكات، المؤقتات، مستمعي الأحداث
- استخدم دوال async داخل التأثيرات، وليس كـ callback للتأثير نفسه
- قم دائمًا بتضمين جميع الاعتماديات لتجنب إغلاقات قديمة