למחזור החיים של קומפוננטת React יש שלושה שלבים: mounting (הקומפוננטה מופיעה על המסך בפעם הראשונה), עדכון (היא מתרנדרת שוב כי ה-state, ה-props או הקונטקסט שלה השתנו) ו-unmounting (היא מוסרת). בקומפוננטות פונקציה מגיבים לשלבים האלה עם useEffect: אפקט עם [] רץ אחרי ה-mount, אפקט עם תלויות רץ אחרי עדכונים שבהם הן השתנו, והפונקציה שאפקט מחזיר רצה ב-unmount.
הקונסול מתחת לתצוגה המקדימה מציג כל שלב כשהוא קורה.
הקונסול מתחיל ב-render, count = 0 וב-mounted. כל לחיצה על Count רושמת רינדור ועדכון. Unmount רושם unmounted; Mount again מתחיל מחדש מ-count = 0, כי ה-state נזרק יחד עם הקומפוננטה.
mounting
קומפוננטה עושה mount בפעם הראשונה ש-React מרנדרת אותה במקום מסוים בעץ. React קוראת לפונקציה, יוצרת את צמתי ה-DOM מה-JSX שהיא מחזירה, מכניסה אותם, נותנת לדפדפן לצייר, ואז מריצה את האפקטים של הקומפוננטה.
useEffect(fn, []) הוא המקום להתחיל דברים שצריכים להתקיים כל עוד הקומפוננטה על המסך: מנוי, טיימר, חיבור, בקשת נתונים ראשונה. הוא רץ אחרי הציור, ולכן הוא אף פעם לא מעכב את הפריים הראשון.
עדכון
קומפוננטה מתעדכנת (מתרנדרת שוב) כש:
- ה-state שלה משתנה,
- ההורה שלה מתרנדר שוב (כברירת מחדל, כל ילד מתרנדר גם הוא, בין אם ה-props שלו השתנו ובין אם לא),
- קונטקסט שהיא קוראת משתנה.
אחרי כל עדכון, React מריצה את האפקטים שהתלויות שלהם השתנו. לפני שהיא מריצה אפקט שוב, היא מריצה את ה-cleanup הקודם של אותו אפקט, כך שהמנוי הישן נסגר לפני שהחדש נפתח. הדף על רינדור מכסה בפירוט רב יותר מה מפעיל רינדור.
unmounting
קומפוננטה עושה unmount כשההורה שלה מפסיק לרנדר אותה (כמו ש-{show && <Counter />} עושה למעלה), כשה-key שלה משתנה, או כשה-root עצמו עושה unmount. React מסירה את צמתי ה-DOM שלה, מריצה את ה-cleanup של כל אפקט, וזורקת את ה-state שלה. אם הקומפוננטה מופיעה שוב מאוחר יותר, זה מופע חדש לגמרי.
הורים וילדים
React מרנדרת מלמעלה למטה אבל מריצה אפקטים מלמטה למעלה. כשהורה וילד עושים mount יחד, האפקטים של הילד רצים קודם, כך שאפקט של הורה יכול לסמוך על כך שהילדים שלו מוכנים לגמרי.
ה-mount רושם render Parent, render Child, Child effect, Parent effect. לחצו על Remove וה-cleanups רצים בסדר ההפוך: Parent cleanup, ואז Child cleanup.
שלבי render ו-commit
כל עדכון עובר שני שלבים:
- שלב ה-render. React קוראת לקומפוננטות שלכם כדי לחשב את ה-JSX החדש ומשווה אותו לתוצאה הקודמת. שום דבר על המסך עוד לא משתנה. React עשויה לעצור את העבודה הזו, להריץ אותה פעמיים ב-StrictMode, או לזרוק אותה, ולכן הרינדור חייב להיות טהור: בלי בקשות, בלי מנויים, בלי שינויים במשתנים חיצוניים.
- שלב ה-commit. React מחילה את ההבדלים על ה-DOM, מחברת refs, ומריצה callbacks של
useLayoutEffect, הכל לפני שהדפדפן מצייר. callbacks שלuseEffectרצים אחרי זה, בדרך כלל אחרי שהדפדפן צייר. כשלחיצה או קלט בדיד אחר גרמו לעדכון, React מריצה אותם במקום זאת מיד אחרי ה-layout effects, כך שהתוצאה מוכנה לפני האירוע הבא.
כל לחיצה רושמת את שלוש השורות באותו סדר. לאפקטים האלה אין מערך תלויות, ולכן הם רצים אחרי כל רינדור. הוסיפו [] כארגומנט שני לשניהם והם רצים רק אחרי הראשון.
מתודות מחזור החיים של class והמקבילות שלהן בהוקים
קומפוננטות class מפרטות את מחזור החיים כמתודות עם שם. תפגשו אותן בקוד ישן וב-error boundaries, שעדיין צריכים class.
| מתודת class | מתי היא רצה | המקבילה בקומפוננטת פונקציה |
|---|---|---|
constructor | פעם אחת, לפני הרינדור הראשון | useState(initial), useRef |
static getDerivedStateFromProps | לפני כל רינדור | לחשב את הערך בזמן הרינדור, או לאפס עם key |
render | בכל רינדור | גוף הפונקציה |
componentDidMount | אחרי שהרינדור הראשון ב-DOM | useEffect(fn, []) |
shouldComponentUpdate | לפני עדכון, יכולה לדלג עליו | React.memo (הוא מחזיר את התשובה ההפוכה: "האם ה-props שווים") |
getSnapshotBeforeUpdate | ממש לפני שה-DOM משתנה | אין הוק ישיר; useLayoutEffect עם ref הוא הכי קרוב |
componentDidUpdate | אחרי כל עדכון | useEffect(fn, [deps]) (רץ גם אחרי mount) |
componentWillUnmount | לפני הסרה | ה-cleanup שמוחזר מ-useEffect |
getDerivedStateFromError, componentDidCatch | כשילד זורק שגיאה | אין: כתבו class של error boundary |
הנה class שרושם כל מתודה כשהיא רצה:
mounting רושם constructor, getDerivedStateFromProps, render, componentDidMount. לחיצה רושמת getDerivedStateFromProps, shouldComponentUpdate, render, getSnapshotBeforeUpdate, componentDidUpdate. גרמו ל-shouldComponentUpdate להחזיר false ולחיצות מפסיקות לעדכן את הכפתור: ה-state משתנה, אבל React מדלגת על הרינדור. הדף על קומפוננטות class מראה איך ממירים class כזה לפונקציה.
חשבו במונחים של סנכרון, לא מחזור חיים
טבלת ההוקים שימושית לקריאת קוד ישן, אבל היא יכולה להוביל אתכם לכתוב אפקטים שמעתיקים הרגלים של class, כמו אפקט "did mount" גדול אחד ואפקט "did update" גדול אחד. עדיף לקרוא אפקט כ-"שמור את הדבר החיצוני הזה מסונכרן עם הערכים האלה": התחבר ל-roomId, וכש-roomId משתנה, התנתק והתחבר שוב. כתבו אפקט אחד לכל דבר שאתם מסנכרנים, רשמו את הערכים שהוא משתמש בהם כתלויות, ותנו ל-React להחליט מתי להריץ אותו.
שאלות נפוצות
מה השלבים במחזור החיים של קומפוננטת React?
mounting (React יוצרת את הקומפוננטה ושמה את הפלט שלה על המסך), עדכון (היא מתרנדרת שוב כי ה-state, ה-props או הקונטקסט שלה השתנו) ו-unmounting (היא מוסרת ומנקה אחריה).
מה המקבילה בהוקים ל-componentDidMount?
useEffect(() => { ... }, []). מערך התלויות הריק גורם לו לרוץ פעם אחת, אחרי שהרינדור הראשון על המסך. הפונקציה שהוא מחזיר היא המקבילה של componentWillUnmount.
מה המקבילה בהוקים ל-componentDidUpdate?
useEffect עם הערכים שחשובים לכם במערך התלויות, כמו [userId]. בניגוד ל-componentDidUpdate, הוא רץ גם אחרי הרינדור הראשון, כך שהוא מכסה mount ועדכון יחד.
באיזה סדר רצים האפקטים של הורה וילד?
React מרנדרת מההורה כלפי מטה, אבל מריצה אפקטים מהילדים כלפי מעלה: האפקט של ילד רץ לפני זה של ההורה שלו. ב-unmount, ה-cleanup של ההורה רץ לפני זה של הילד.
מה הם שלבי ה-render וה-commit?
בשלב ה-render React קוראת לקומפוננטות שלכם כדי לחשב איך המסך צריך להיראות; היא יכולה לעצור את העבודה הזו או לזרוק אותה. בשלב ה-commit היא מחילה את השינויים על ה-DOM ומריצה layout effects; אפקטים רגילים רצים אחרי זה, בדרך כלל אחרי שהדפדפן צייר.