6.8 useEffect הרצאה
תופעות לוואי - side effects¶
קומפוננטת ריאקט היא פונקציה שמקבלת props ומחזירה JSX. אבל לפעמים צריך לעשות דברים שהם מעבר לרינדור - לגשת לשרת, להאזין לאירועים, לעדכן את הכותרת, להפעיל טיימר. אלה נקראים תופעות לוואי (side effects).
דוגמאות לתופעות לוואי:
- שליפת נתונים מ-API (fetch)
- הרשמה לאירועים (addEventListener)
- שינוי כותרת הדף (document.title)
- טיימרים (setTimeout, setInterval)
- אינטגרציה עם ספריות חיצוניות
useEffect - הוק לתופעות לוואי¶
useEffect מאפשר להריץ קוד אחרי שריאקט סיימה לרנדר את הקומפוננטה:
import { useEffect, useState } from "react";
function PageTitle() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
});
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
useEffect מקבל פונקציה שרצה אחרי כל רינדור. במקרה הזה, בכל פעם שהקומפוננטה מרונדרת, כותרת הדף מתעדכנת.
מערך תלויות - dependency array¶
בלי מערך תלויות, ה-effect רץ אחרי כל רינדור. אפשר לשלוט מתי הוא רץ עם הפרמטר השני:
ריצה בכל רינדור¶
ריצה רק בטעינה ראשונה¶
מערך תלויות ריק - ה-effect רץ רק פעם אחת, כשהקומפוננטה עולה (mount):
ריצה כשערך ספציפי משתנה¶
מערך תלויות עם ערכים - ה-effect רץ רק כשאחד מהערכים משתנה:
הכלל: כל משתנה שה-effect משתמש בו ומגיע מהקומפוננטה (סטייט, props, משתנים מחושבים) חייב להיות במערך התלויות.
פונקציית ניקוי - cleanup function¶
effect יכול להחזיר פונקציה שרצה לפני ה-effect הבא, ולפני שהקומפוננטה נמחקת (unmount):
useEffect(() => {
// setup
const timer = setInterval(() => {
console.log("tick");
}, 1000);
// cleanup - runs before next effect and on unmount
return () => {
clearInterval(timer);
};
}, []);
למה צריך cleanup¶
בלי cleanup, משאבים יכולים לדלוף:
- טיימרים ממשיכים לרוץ אחרי שהקומפוננטה נמחקה
- event listeners נשארים רשומים
- בקשות HTTP חוזרות גם כשהתוצאה כבר לא רלוונטית
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>Window width: {width}px</p>;
}
מערך תלויות ריק ([]) כי רוצים להירשם לאירוע פעם אחת בלבד. ה-cleanup מסיר את ה-listener כשהקומפוננטה נמחקת.
הזמנים של effect ו-cleanup¶
Mount: effect runs
Re-render: cleanup runs -> effect runs
Re-render: cleanup runs -> effect runs
Unmount: cleanup runs
שליפת נתונים - fetching data¶
אחד השימושים הנפוצים ביותר של useEffect הוא שליפת נתונים מ-API:
interface User {
id: number;
name: string;
email: string;
}
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => {
if (!res.ok) throw new Error("Failed to fetch");
return res.json();
})
.then((data: User[]) => {
setUsers(data);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
שליפה עם async/await¶
useEffect לא יכול להיות async ישירות. צריך ליצור פונקציה async בתוכו:
useEffect(() => {
const fetchUsers = async () => {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
if (!res.ok) throw new Error("Failed to fetch");
const data: User[] = await res.json();
setUsers(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
למה לא useEffect(async () => ...)? כי פונקציה async מחזירה Promise, אבל useEffect מצפה שהפונקציה תחזיר void או פונקציית cleanup.
שליפה שתלויה בפרמטר¶
כשהנתונים תלויים בערך שמשתנה (למשל id של משתמש), מוסיפים אותו למערך התלויות:
interface Post {
id: number;
title: string;
body: string;
}
function UserPosts({ userId }: { userId: number }) {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`)
.then((res) => res.json())
.then((data: Post[]) => {
setPosts(data);
setLoading(false);
});
}, [userId]); // re-fetch when userId changes
if (loading) return <p>Loading posts...</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
כל פעם ש-userId משתנה, ה-effect רץ מחדש ושולף את הפוסטים של המשתמש החדש.
ביטול בקשות עם AbortController¶
כשהמשתמש משנה את הערך מהר, בקשות ישנות עדיין בדרך ויכולות לדרוס תוצאות חדשות. AbortController פותר את זה:
useEffect(() => {
const controller = new AbortController();
const fetchPosts = async () => {
setLoading(true);
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`,
{ signal: controller.signal }
);
const data: Post[] = await res.json();
setPosts(data);
} catch (err) {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchPosts();
return () => {
controller.abort(); // cancel previous request
};
}, [userId]);
ה-cleanup מבטל את הבקשה הקודמת כשה-effect רץ מחדש.
דפוסים נפוצים¶
טיימר עם cleanup¶
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (!isRunning) return;
const interval = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, [isRunning]);
return (
<div>
<p>Time: {seconds}s</p>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? "Stop" : "Start"}
</button>
<button onClick={() => { setIsRunning(false); setSeconds(0); }}>
Reset
</button>
</div>
);
}
שמירה ב-localStorage¶
function PersistentCounter() {
const [count, setCount] = useState(() => {
const saved = localStorage.getItem("count");
return saved ? Number(saved) : 0;
});
useEffect(() => {
localStorage.setItem("count", String(count));
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
הערך ההתחלתי נקרא מ-localStorage (עם lazy initializer). בכל פעם ש-count משתנה, הוא נשמר ב-localStorage.
האזנה ללחיצת מקשים¶
function KeyListener() {
const [lastKey, setLastKey] = useState("");
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
setLastKey(e.key);
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return <p>Last key pressed: {lastKey || "None"}</p>;
}
טעויות נפוצות¶
שכחה של תלויות¶
// BAD - count is always 0 inside the effect
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // stale closure - count is always 0
}, 1000);
return () => clearInterval(interval);
}, []); // count is missing from dependencies!
// GOOD - use functional update
useEffect(() => {
const interval = setInterval(() => {
setCount((prev) => prev + 1); // always uses latest value
}, 1000);
return () => clearInterval(interval);
}, []);
effect אינסופי¶
// BAD - infinite loop!
const [data, setData] = useState<string[]>([]);
useEffect(() => {
setData([...data, "new item"]); // changes data, which triggers effect again
}, [data]); // data is a dependency, but effect changes data
// GOOD - use functional update to avoid dependency
useEffect(() => {
setData((prev) => [...prev, "new item"]);
}, []); // no dependency on data
שכחת cleanup¶
// BAD - event listener leaks
useEffect(() => {
window.addEventListener("resize", handleResize);
// forgot to return cleanup!
}, []);
// GOOD
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
סיכום¶
useEffectמריץ קוד אחרי הרינדור - עבור תופעות לוואי כמו fetch, timers, events- מערך תלויות שולט מתי ה-effect רץ:
[]פעם אחת,[x]כש-x משתנה, בלי מערך בכל רינדור - כל משתנה שה-effect משתמש בו חייב להיות במערך התלויות
- פונקציית cleanup רצה לפני ה-effect הבא ובעת unmount - חשוב לנקות timers, listeners, ובקשות
- שליפת נתונים: useEffect עם מערך תלויות ריק (או עם הפרמטרים שמשתנים)
useEffectלא יכול להיות async - יוצרים פונקציה async בתוכו- ביטול בקשות עם AbortController מונע race conditions
- עדכון פונקציונלי (
prev => ...) מונע תלות מיותרת בסטייט