מחזור חיים ו-useEffect
מבוא
Lifecycle (מחזור חיים) הוא שם כולל לשלבים שכל קומפוננטה עוברת מרגע הופעתה על המסך ועד הסרתה: יצירה (Mount), עדכון (Update) והריסה (Unmount). הבנת המחזור הזה חיונית כדי לדעת מתי לטעון נתונים, מתי להגיב לשינויים ומתי לשחרר משאבים.
בקומפוננטות פונקציונליות מנהלים את כל השלבים האלה באמצעות Hook יחיד - useEffect - שבו נתמקד בשיעור הזה.
מהו useEffect?
useEffect הוא Hook שמאפשר לבצע side effects - פעולות שמסנכרנות את הקומפוננטה עם משהו שנמצא מחוץ לה. בין השימושים הנפוצים:
- קריאות לשרת (API)
- הרשמה לאירועים והאזנה להם (subscriptions)
- שינויים ידניים ב-DOM
- טיימרים ומרווחי זמן
התחביר הבסיסי
import { useEffect } from 'react';
useEffect(() => {
// הקוד שירוץ (effect)
return () => {
// cleanup (אופציונלי) - רץ לפני הריסה או לפני האפקט הבא
};
}, [dependencies]); // מערך תלויות
מערך התלויות (Dependencies Array)
מערך התלויות קובע מתי האפקט ירוץ:
1. ללא מערך - רץ בכל רינדור
useEffect(() => {
console.log('רץ בכל רינדור!');
});
// ⚠️ זהירות! עלול לגרום לבעיות ביצועים
2. מערך ריק [] - רץ פעם אחת (Mount)
useEffect(() => {
console.log('רץ רק בטעינה הראשונה');
// שווה ערך ל-componentDidMount
}, []);
3. עם תלויות - רץ כשהתלות משתנה
useEffect(() => {
console.log(`הערך השתנה ל: ${value}`);
// שווה ערך ל-componentDidUpdate (עבור value)
}, [value]);
4. מספר תלויות
useEffect(() => {
console.log('אחד מהערכים השתנה');
}, [value1, value2, value3]);
השוואה ל-Class Lifecycle
| Class Component | Function Component + useEffect |
|---|---|
componentDidMount |
useEffect(() => {}, []) |
componentDidUpdate |
useEffect(() => {}, [deps]) |
componentWillUnmount |
useEffect(() => { return () => {} }, []) |
דוגמה מפורטת
// Class Component (סגנון ישן)
class Example extends React.Component {
componentDidMount() {
console.log('נטען');
document.title = 'נטען';
}
componentDidUpdate(prevProps) {
if (prevProps.count !== this.props.count) {
console.log('count השתנה');
}
}
componentWillUnmount() {
console.log('נהרס');
}
}
// Function Component (סגנון מודרני)
function Example({ count }) {
// componentDidMount
useEffect(() => {
console.log('נטען');
document.title = 'נטען';
}, []);
// componentDidUpdate for count
useEffect(() => {
console.log('count השתנה');
}, [count]);
// componentWillUnmount
useEffect(() => {
return () => {
console.log('נהרס');
};
}, []);
}
Cleanup Function
פונקציית ה-cleanup היא פונקציה שמחזירים מתוך האפקט, ותפקידה לנקות אחריו. היא רצה בשני מצבים:
- לפני הריסת הקומפוננטה (Unmount)
- לפני כל הרצה חוזרת של האפקט, כדי לבטל את ההרצה הקודמת
למה צריך Cleanup?
// ❌ בעיה: דליפת זיכרון (Memory Leak)
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// הטיימר ממשיך לרוץ גם אחרי שהקומפוננטה נהרסת!
}, []);
return <p>{seconds}</p>;
}
// ✅ פתרון: Cleanup
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup - עוצר את הטיימר
return () => {
clearInterval(interval);
};
}, []);
return <p>{seconds}</p>;
}
דוגמאות נוספות ל-Cleanup
// Event Listeners
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// WebSocket
useEffect(() => {
const socket = new WebSocket('wss://example.com');
socket.onmessage = (event) => console.log(event.data);
return () => {
socket.close();
};
}, []);
// AbortController (לביטול fetch)
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') {
setError(err.message);
}
});
return () => {
controller.abort();
};
}, []);
Memory Leaks - דליפות זיכרון
דליפת זיכרון (Memory Leak) מתרחשת כשהקוד ממשיך לרוץ או להחזיק במשאבים גם אחרי שהקומפוננטה כבר נהרסה. הסיבות השכיחות:
- טיימרים שלא נעצרים
- מאזיני אירועים (Event listeners) שלא מוסרים
- קריאות API שלא מבוטלות
- הרשמות (Subscriptions) שלא מנותקות
סימנים לדליפת זיכרון
Warning: Can't perform a React state update on an unmounted component.
מניעת דליפות
function DataFetcher({ url }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true; // דגל לבדיקה
async function fetchData() {
try {
const res = await fetch(url);
const json = await res.json();
// עדכון רק אם הקומפוננטה עדיין קיימת
if (isMounted) {
setData(json);
setLoading(false);
}
} catch (error) {
if (isMounted) {
setLoading(false);
}
}
}
fetchData();
return () => {
isMounted = false; // סימון שהקומפוננטה נהרסה
};
}, [url]);
if (loading) return <p>טוען...</p>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
דפוסים נפוצים
1. Fetch בטעינה
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(setUsers);
}, []);
2. עדכון כותרת הדף
useEffect(() => {
document.title = `${count} הודעות חדשות`;
}, [count]);
3. הרשמה לאירועי חלון
useEffect(() => {
const handleScroll = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
4. Debounce בחיפוש
useEffect(() => {
const timeoutId = setTimeout(() => {
if (searchTerm) {
searchAPI(searchTerm).then(setResults);
}
}, 300);
return () => clearTimeout(timeoutId);
}, [searchTerm]);
5. סנכרון עם localStorage
useEffect(() => {
localStorage.setItem('theme', theme);
}, [theme]);
טעויות נפוצות
1. שכחתם תלויות
// ❌ שגוי - count לא במערך התלויות
useEffect(() => {
console.log(count); // תמיד יהיה הערך ההתחלתי
}, []);
// ✅ נכון
useEffect(() => {
console.log(count);
}, [count]);
2. לולאה אינסופית
// ❌ שגוי - גורם ללולאה אינסופית
useEffect(() => {
setData([...data, newItem]); // data משתנה → אפקט רץ שוב → ...
}, [data]);
// ✅ נכון - שימוש ב-callback
useEffect(() => {
setData(prev => [...prev, newItem]);
}, [newItem]);
3. אובייקט/מערך כתלות
// ❌ בעייתי - אובייקט חדש בכל רינדור
const options = { page: 1, limit: 10 };
useEffect(() => {
fetch('/api', options);
}, [options]); // רץ בכל רינדור!
// ✅ פתרון 1: useMemo
const options = useMemo(() => ({ page, limit }), [page, limit]);
// ✅ פתרון 2: תלויות פרימיטיביות
useEffect(() => {
fetch('/api', { page, limit });
}, [page, limit]);
סיכום
| מצב | תחביר |
|---|---|
| Mount (פעם אחת) | useEffect(() => {}, []) |
| Update (כשמשתנה) | useEffect(() => {}, [dep]) |
| Unmount (cleanup) | useEffect(() => () => {}, []) |
| כל רינדור | useEffect(() => {}) |
הקודם: ← תרגילים
הבא: Props Drilling & Lifting State →
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
למה מחזירים פונקציה מתוך
useEffect?הצגת התשובה
תשובה א. בלי ניקוי נוצרות דליפות זיכרון ומאזינים כפולים.
-
מה שקול ל-componentDidMount?
הצגת התשובה
תשובה ג. מערך תלויות ריק רץ פעם אחת אחרי ההרכבה.