Menu

רינדור ב-React: מה גורם לרינדור מחדש ולמה

קומפוננטת React מתרנדרת שוב כשה-state שלה משתנה, כשההורה שלה מתרנדר, או כשקונטקסט שהיא קוראת משתנה. למדו את ההבדל בין render ל-commit, מה ה-virtual DOM באמת אומר, ואיך עוצרים רינדורים שלא צריך.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

קומפוננטת React מתרנדרת מחדש כשה-state שלה משתנה, כשההורה שלה מתרנדר מחדש, או כשקונטקסט שהיא קוראת משתנה. רינדור פירושו ש-React קוראת שוב לפונקציית הקומפוננטה כדי לקבל JSX חדש; אחר כך היא מעדכנת רק את החלקים בדף שבאמת השתנו.

פתחו את ה-Console מתחת לתצוגה המקדימה ולחצו על הכפתור. שלוש הקומפוננטות רושמות, כולל Title, שלא מקבלת props בכלל. היא התרנדרה כי ההורה שלה התרנדר.

מה מפעיל רינדור

React מרנדרת קומפוננטה בדיוק מהסיבות האלה:

  1. הרינדור הראשון. האפליקציה מתחילה, או שהקומפוננטה מופיעה בעץ בפעם הראשונה.
  2. ה-state שלה השתנה. קראתם ל-setter מ-useState או ל-dispatch מ-useReducer עם ערך חדש.
  3. ההורה שלה התרנדר. כברירת מחדל, כשקומפוננטה מתרנדרת, כל קומפוננטה שהיא מחזירה מתרנדרת גם היא, עד הסוף למטה.
  4. קונטקסט שהיא קוראת השתנה. קומפוננטה שקוראת ל-useContext(SomeContext) מתרנדרת כשה-provider הקרוב מעביר value חדש (ראו useContext).

אמונה נפוצה היא שקומפוננטה מתרנדרת "כי ה-props שלה השתנו". זה הופך את הסיבה. props הם ארגומנטים שההורה מעביר בזמן שהוא מתרנדר, ולכן props חדשים יכולים להגיע רק כשההורה מתרנדר. ורינדור של ההורה מספיק בפני עצמו: Title שלמעלה בלי props ועדיין מתרנדרת בכל פעם.

קביעת state לערך שכבר יש לו (בהשוואה עם Object.is) לא מתחילה רינדור של הילדים. React עשויה עדיין לקרוא לאותה קומפוננטה אחת פעם אחת לפני שהיא שמה לב ששום דבר לא השתנה, אבל היא זורקת את התוצאה.

render ו-commit

כל עדכון עובר שני שלבים:

  • render. React קוראת לקומפוננטות שלכם. הן מחזירות JSX, שהוא סתם אובייקטים שמתארים מה המסך צריך להציג. שום דבר בדף עוד לא משתנה, ולכן ה-render חייב להיות טהור: בלי כתיבה ל-DOM, בלי בקשות, בלי שינוי משתנים מחוץ לקומפוננטה.
  • commit. React משווה את הפלט החדש לקודם ומחילה את ההבדלים על ה-DOM: היא מכניסה, מסירה או מעדכנת רק את הצמתים שהשתנו. אחר כך הדפדפן מצייר, ואחרי זה React מריצה את האפקטים שלכם.

הדוגמה שמתחת מתרנדרת בכל לחיצה, אבל React שומרת את אותו אלמנט <input>. אפקט רץ אחרי כל commit ובודק את זה.

הקלידו משהו בשדה ולחצו כמה פעמים. הטקסט שהקלדתם נשאר, כי React אף פעם לא החליפה את השדה: השינוי היחיד ב-DOM בכל לחיצה הוא המספר בתוך ה-<p>. הלוג גם מראה את הסדר, קודם render ואחרי ה-commit האפקט.

ה-virtual DOM, בפשטות

"Virtual DOM" הוא השם הפופולרי לאובייקטים שהקומפוננטות שלכם מחזירות. <p>Items: {count}</p> מתקמפל לקריאה שיוצרת אובייקט כמו { type: 'p', props: { children: ['Items: ', 1] } }. אחרי רינדור, React עוברת על העץ החדש של האובייקטים האלה לצד הקודם, ובמקום שבו הטיפוס והמיקום תואמים היא שומרת את צומת ה-DOM הקיים ומעדכנת רק מאפיינים וטקסט שהשתנו. ההשוואה הזו נקראת reconciliation.

המונח רופף משתי סיבות. React לא מחזיקה עותק שני של ה-DOM ולא משווה שני DOMs: היא משווה אובייקטי אלמנטים מול העץ הפנימי שלה של קומפוננטות (עץ ה-fiber). ואותו תהליך מניע יעדים שאין להם DOM בכלל, כמו React Native. התיעוד של React ברובו נמנע מהביטוי ומדבר על render ו-commit במקום. מה שחשוב בפועל הוא התוצאה: רינדור זול לעומת עבודה עם ה-DOM, כי רוב הרינדורים מסתיימים בעדכון DOM קטן או בכלום.

שני כללים נובעים מהאופן שבו ההשוואה עובדת. טיפוס אלמנט אחר באותו מקום (<div> שמוחלף ב-<section>, או ComponentA ב-ComponentB) הורס את תת העץ הישן ואת ה-state שלו. וברשימות, key אומר ל-React איזה פריט הוא איזה, כדי שתוכל להזיז צמתים במקום לבנות אותם מחדש.

לעצור רינדורים שלא צריך

רוב הרינדורים הנוספים לא עולים שום דבר מורגש. כשאחד כן כואב, למשל רשימה גדולה שמתרנדרת בכל הקשה במקום אחר בדף, נסו את אלה לפי הסדר.

הורידו את ה-state למטה

אם רק חלק קטן מהמסך משתמש בחלק state מסוים, החזיקו את ה-state הזה בקומפוננטה שעוטפת רק את החלק הזה. הגרסה הזו מרנדרת מחדש את ProductList בכל הקשה:

export default function App() {
    const [text, setText] = useState('');
    return (
        <>
            <input value={text} onChange={(e) => setText(e.target.value)} />
            <ProductList />
        </>
    );
}

העבירו את השדה ואת ה-state שלו לקומפוננטה משלהם, והרשימה כבר לא נמצאת בתוך הקומפוננטה שמתרנדרת:

הקלידו כמה אותיות: רק SearchBox רושמת.

העבירו children במקום

לפעמים ה-state צריך לחיות בעטיפה שמסביב לחלק היקר, כמו פאנל שמחליף הדגשה. תנו לעטיפה לקבל children. ההורה יוצר את אלמנטי הילדים, ומכיוון שההורה לא מתרנדר כשה-state של העטיפה משתנה, האלמנטים האלה הם אותם אובייקטים כמו קודם, ולכן React מדלגת עליהם.

לחצו על ההחלפה ורק Highlighter רושמת. עכשיו העבירו את <Article /> לתוך ה-JSX של Highlighter במקום {children}, וכל לחיצה רושמת גם render Article.

memoization

כששום ארגון מחדש לא מתאים, עטפו את הילד ב-memo. React משווה אז את ה-props שלו לקודמים ומדלגת על הרינדור כשכולם זהים. אובייקטים ופונקציות שנוצרים בזמן הרינדור הם חדשים בכל פעם, ולכן memo מגיע בדרך כלל עם useMemo או useCallback בשביל ה-props האלה. הדף על React.memo מראה את זה עם דוגמאות שרצות.

import { memo } from 'react';

const ProductList = memo(function ProductList({ category }) {
    // skipped while category stays the same
});

React Compiler יכול להוסיף memoization כזה בשבילכם בזמן ה-build, אבל ארגון מחדש של ה-state הוא עדיין הדבר הראשון לנסות: הוא מבטל את העבודה במקום לשמור אותה ב-cache.

רינדורים הם בדרך כלל בסדר

רינדור הוא קריאה לפונקציה שמחזירה אובייקטים. React מריצה אלפים מהם מהר, ורינדור שמייצר את אותו פלט לא משנה שום DOM. אל תוסיפו memo בכל מקום "ליתר ביטחון": לכל השוואה יש עלות משלה והיא הופכת את הקוד לקשה יותר לקריאה. מדדו קודם. ה-Profiler של React DevTools מראה אילו קומפוננטות התרנדרו, למה, וכמה זמן לקח כל אחת.

שאלות נפוצות

מה גורם לקומפוננטת React להתרנדר מחדש?

שלושה דברים: ה-state שלה משתנה, ההורה שלה מתרנדר, או שקונטקסט שהיא קוראת עם useContext מקבל ערך חדש. props הם לא טריגר נפרד: props חדשים מגיעים רק כי ההורה התרנדר.

האם ילד מתרנדר מחדש כשההורה מתרנדר מחדש?

כן, כברירת מחדל כל קומפוננטה בתוך הורה שמתרנדר מתרנדרת גם היא, גם אם ה-props שלה לא השתנו. עטיפת הילד ב-memo מאפשרת ל-React לדלג עליו כשה-props שלו זהים לפעם הקודמת.

מה זה ה-virtual DOM ב-React?

זה שם רופף לעץ של אובייקטי JavaScript רגילים (React elements) שהקומפוננטות שלכם מחזירות. React משווה את העץ החדש לקודם ומשנה רק את צמתי ה-DOM האמיתיים שיש בהם הבדל.

האם רינדור מחדש פוגע בביצועים?

בדרך כלל לא. רינדור הוא קריאה לפונקציה שמייצרת אובייקטים, ו-React נוגעת ב-DOM רק במקום שבו הפלט השתנה. בצעו אופטימיזציה כשרינדור איטי באופן מדיד, למשל עם ה-Profiler של React DevTools.

מה ההבדל בין render ל-commit?

רינדור הוא React שקוראת לקומפוננטות שלכם כדי לדעת איך המסך צריך להיראות. commit הוא React שמחילה את ההבדלים על ה-DOM. רינדור שמייצר את אותו פלט לא עושה commit לשום שינוי ב-DOM.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל