useEffect מריץ קטע קוד אחרי ש-React רינדרה את הקומפוננטה ועדכנה את המסך. משתמשים בו כדי לשמור קומפוננטה מסונכרנת עם משהו מחוץ ל-React, כמו טיימר, חיבור רשת או event listener, ושולטים מתי הוא רץ שוב בעזרת מערך תלויות.
פתחו את הקונסול מתחת לתצוגה המקדימה: האפקט רושם פעם אחת כשהקומפוננטה מופיעה, ואז שוב אחרי כל לחיצה. שנו את [count] ל-[] ולחצו שוב: האפקט שותק אחרי הרישום הראשון.
התחביר
useEffect(() => {
// setup: runs after the render is on the screen
return () => {
// cleanup: runs before the next setup, and on unmount
};
}, [dependencies]);
- הארגומנט הראשון הוא פונקציית ה-setup. היא יכולה להחזיר פונקציית cleanup, או כלום.
- הארגומנט השני הוא מערך התלויות: כל ערך מהקומפוננטה שה-setup קורא (props, state, וכל מה שמחושב מהם).
- קראו ל-
useEffectברמה העליונה של הקומפוננטה, כמו לכל הוק.
אפקטים רצים אחרי הרינדור
רינדור הוא React שקוראת לפונקציית הקומפוננטה שלכם כדי לדעת מה המסך צריך להציג. אפקטים מחכים עד שהעבודה הזו הסתיימה וה-DOM התעדכן, כך שבתוך אפקט הדף כבר מציג את הערכים החדשים.
כל לחיצה רושמת קודם את הרינדור ואחר כך את האפקט, והאפקט קורא את הטקסט של הכפתור ש-React בדיוק כתבה. לכן קריאה מה-DOM, הפעלת טיימר או התחברות לשרת שייכות לאפקט ולא לגוף הקומפוננטה: הגוף רץ בזמן הרינדור, לפני שמשהו נמצא על המסך, ו-React עשויה לקרוא לו יותר פעמים ממה שאתם מצפים.
אפקטים רצים בדרך כלל אחרי שהדפדפן צייר. כשאפקט חייב למדוד את ה-DOM לפני הציור, השתמשו ב-useLayoutEffect במקום, שתמיד רץ לפני הציור.
מערך התלויות: שלוש צורות
מערך התלויות מחליט מתי האפקט רץ שוב. יש שלוש צורות, והבלוק הזה משתמש בשלושתן כדי שתוכלו לראות את ההבדל.
בטעינה שלושת האפקטים רושמים. לחצו על a + 1 ושניים מהם רצים: זה שאין לו מערך וזה שתלוי ב-a. לחצו על b + 1 ורק זה שאין לו מערך רץ, כי שום דבר ב-[] או ב-[a] לא השתנה.
| מערך תלויות | מתי האפקט רץ |
|---|---|
| אין | אחרי כל רינדור |
[] | פעם אחת, אחרי הרינדור הראשון (ב-mount) |
[a, b] | אחרי הרינדור הראשון, ואז בכל פעם ש-a או b השתנו |
React משווה כל תלות לערך הקודם שלה בעזרת Object.is. מספרים, מחרוזות ובוליאנים מושווים לפי ערך. אובייקטים, מערכים ופונקציות מושווים לפי זהות, וזה משנה הרבה (ראו בהמשך).
אל תשמיטו ערך מהמערך כדי שאפקט ירוץ פחות. האפקט היה ממשיך להשתמש בערך מרינדור ישן. כלל ה-lint react-hooks/exhaustive-deps תופס את זה; הוא חלק מהגדרת ה-ESLint שתבנית ה-React של Vite והגדרות ה-ESLint של Next.js כוללות.
cleanup
הרבה אפקטים מתחילים משהו שצריך לעצור: חיבור, מנוי, טיימר. החזירו פונקציה מהאפקט ו-React קוראת לה בשני רגעים: רגע לפני שהאפקט רץ שוב עם תלויות חדשות, וכשהקומפוננטה עוזבת את המסך (unmount).
בחרו חדר אחר: הקונסול מציג disconnect from general ואז connect to music. החיבור הישן תמיד נסגר לפני שהחדש נפתח. לחצו על Hide וה-cleanup רץ פעם אחרונה, כי ל-ChatRoom נעשה unmount.
הפונקציה connect כאן מחליפה לקוח צ'אט או WebSocket אמיתי. הצורה זהה בכל אחד מהם: יוצרים ב-setup, סוגרים ב-cleanup.
ניקוי טיימר
טיימר שהופעל באפקט ממשיך לרוץ אחרי שהקומפוננטה נעלמה, אלא אם ה-cleanup עוצר אותו.
מחקו את השורה clearInterval(id);, ואז הסתירו את הטיימר: tick ממשיך להגיע לקונסול מקומפוננטה שכבר לא קיימת. כל Show מפעיל עוד אחד.
שימו לב ל-setSeconds((s) => s + 1). האפקט רץ פעם אחת, ולכן setSeconds(seconds + 1) רגיל היה קורא את seconds מהרינדור הראשון לנצח ונעצר ב-1. פונקציית ה-updater תמיד מקבלת את הערך העדכני.
הסרת event listener
event listeners על window או document עוקבים אחרי אותו דפוס. העבירו ל-removeEventListener את אותה פונקציה שהעברתם ל-addEventListener:
useEffect(() => {
function handleKeyDown(e) {
if (e.key === 'Escape') onClose();
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
הלולאה האינסופית
אפקט שקובע ערך state שהוא גם תלוי בו מתחיל לולאה: ה-state משתנה, React מרנדרת, התלות השתנתה, האפקט רץ וקובע את ה-state שוב.
// Never stops: every render runs the effect, every effect causes a render
useEffect(() => {
setCount(count + 1);
});
// Same loop: count changes, so the effect runs again
useEffect(() => {
setCount(count + 1);
}, [count]);
הקומפוננטה מתרנדרת בלי סוף והטאב נשאר עסוק. React לא עוצרת את הלולאה הזו: בפיתוח היא רושמת לקונסול שגיאת "Maximum update depth exceeded", ובפרודקשן היא לא רושמת כלום. הבלוק הזה מריץ את הגרסה השנייה עם תנאי עצירה, כדי שתוכלו לראות את הלולאה בלי להקפיא את הדף:
הקונסול רושם מ-render 0 עד render 5: שישה רינדורים ל-mount אחד, כל אחד מהם הופעל על ידי האפקט. התיקונים תלויים במה שהתכוונתם:
- רציתם פעם אחת. השתמשו ב-
[]ובצורת ה-updater,setCount((c) => c + 1), כך ש-countהוא לא תלות. - אתם מחשבים ערך מ-state אחר. אל תשמרו אותו בכלל; חשבו אותו בזמן הרינדור, כמו שמוצג בסעיף האחרון בדף הזה.
- אתם מגיבים לפעולה של המשתמש. שימו את הקוד ב-event handler.
אובייקטים ופונקציות כתלויות
אובייקט, מערך או פונקציה שנוצרים בגוף הקומפוננטה הם ערך חדש בכל רינדור, ולכן אפקט שתלוי בהם רץ אחרי כל רינדור, גם כששום דבר בתוכם לא השתנה.
החלפת הצבעים לא קשורה לחיפוש, ובכל זאת כל החלפה רושמת שוב search for react. העבירו את שורת ה-options לתוך האפקט ושנו את המערך ל-[query]: ההחלפות שותקות ורק הקלדה בשדה מריצה את החיפוש.
זה התיקון הכללי. צרו אובייקטים ופונקציות בתוך האפקט שמשתמש בהם, והיו תלויים בערכים הפרימיטיביים שמהם הם נבנים. כשפונקציה חייבת לחיות מחוץ לאפקט, useCallback שומר על הזהות שלה יציבה בין רינדורים.
למה אפקטים רצים פעמיים בפיתוח
כש-<StrictMode> פעיל (אפליקציית Vite חדשה עוטפת בו את ה-root, וה-App Router של Next.js מפעיל אותו כברירת מחדל), React מריצה בדיקה בפיתוח: היא עושה mount לכל קומפוננטה, מריצה את האפקטים שלה, מריצה את ה-cleanups שלהם, ומריצה את האפקטים שוב. עבור חדר הצ'אט שלמעלה, קונסול הפיתוח מציג:
connect to general
disconnect from general
connect to general
זה קורה פעם אחת, ב-mount, ורק בפיתוח. זה קיים כדי לחשוף אפקטים שחסר להם cleanup: אם שתי הרצות ברצף שוברות משהו, גם המשתמש היה שובר את זה כשהוא יוצא וחוזר. התיקון הוא אף פעם לא לחסום את ההרצה השנייה עם ref; התיקון הוא לכתוב את ה-cleanup. התצוגה המקדימה בדף הזה רצה כמו build של פרודקשן, אז לא תראו כאן את ההרצה הכפולה.
אולי לא צריך אפקט
אפקטים נועדו לסנכרון עם מערכות מחוץ ל-React. שני שימושים נפוצים טובים יותר בלי אפקט.
ערכים שמחושבים מ-props או מ-state. חשבו אותם בזמן הרינדור. שמירה שלהם ב-state וסנכרון עם אפקט עולים ברינדור נוסף, והראשון מבין הרינדורים האלה שם ב-DOM ערך ישן:
כל לחיצה מרנדרת את WithEffect פעמיים, והרינדור הראשון שלה עדיין מציג את השם הישן. Computed מתרנדרת פעם אחת עם הערך הנכון.
קוד שמגיב לפעולה של המשתמש. שליחת טופס, הצגת toast אחרי רכישה, או שליחת אירוע אנליטיקס על לחיצה שייכים ל-event handler. שם יודעים בדיוק מה קרה, ואילו אפקט יודע רק שערך כלשהו השתנה.
useEffectEvent
לפעמים אפקט צריך את הערך העדכני של prop בלי לרוץ מחדש כשה-prop הזה משתנה. בחדר הצ'אט, התראה צריכה להשתמש ב-theme הנוכחי, אבל החלפת ערכת הנושא לא צריכה לחבר מחדש. React 19.2 מוסיפה לשם כך את useEffectEvent:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // always the latest theme
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => onConnected());
connection.connect();
return () => connection.disconnect();
}, [roomId]); // theme is not a dependency
}
פונקציה מ-useEffectEvent מיועדת רק לקריאה בתוך אפקטים. אל תעבירו אותה לקומפוננטות אחרות ואל תרשמו אותה במערך תלויות.
שאלות נפוצות
מתי useEffect רץ?
אחרי ש-React רינדרה את הקומפוננטה ועדכנה את ה-DOM, בדרך כלל אחרי שהדפדפן צייר. בלי מערך תלויות הוא רץ אחרי כל רינדור, עם [] רק אחרי הראשון, ועם [a, b] אחרי הרינדור הראשון ואחרי כל רינדור שבו a או b השתנו.
מה זו פונקציית ה-cleanup של useEffect?
הפונקציה שמחזירים מהאפקט. React קוראת לה לפני שהיא מריצה את האפקט שוב עם תלויות חדשות, ועוד פעם כשהקומפוננטה מוסרת מהמסך. השתמשו בה כדי לנקות טיימרים, להסיר event listeners ולסגור מנויים.
למה ה-useEffect שלי רץ פעמיים?
בפיתוח, <StrictMode> עושה mount לכל קומפוננטה, מריץ את האפקטים שלה, מנקה אותם ומריץ אותם שוב, כדי להראות לכם אפקטים שחסר להם cleanup. זה קורה פעם אחת ב-mount ורק בפיתוח. builds של פרודקשן מריצים את האפקט פעם אחת.
למה ה-useEffect שלי גורם ללולאה אינסופית?
האפקט קובע ערך state שנמצא גם במערך התלויות שלו (או שאין לו מערך בכלל). קביעת ה-state מרנדרת שוב, התלות השתנתה, ולכן האפקט רץ שוב. השתמשו בפונקציית updater והסירו את התלות, הוסיפו תנאי, או חשבו את הערך בזמן הרינדור במקום.
האם ה-callback של useEffect יכול להיות async?
לא. ה-callback חייב להחזיר כלום או פונקציית cleanup, ופונקציית async מחזירה promise. הגדירו פונקציה async בתוך האפקט וקראו לה, כמו שמוצג בדף על טעינת נתונים.