קומפוננטת React מתרנדרת מחדש כשה-state שלה משתנה, כשההורה שלה מתרנדר מחדש, או כשקונטקסט שהיא קוראת משתנה. רינדור פירושו ש-React קוראת שוב לפונקציית הקומפוננטה כדי לקבל JSX חדש; אחר כך היא מעדכנת רק את החלקים בדף שבאמת השתנו.
פתחו את ה-Console מתחת לתצוגה המקדימה ולחצו על הכפתור. שלוש הקומפוננטות רושמות, כולל Title, שלא מקבלת props בכלל. היא התרנדרה כי ההורה שלה התרנדר.
מה מפעיל רינדור
React מרנדרת קומפוננטה בדיוק מהסיבות האלה:
- הרינדור הראשון. האפליקציה מתחילה, או שהקומפוננטה מופיעה בעץ בפעם הראשונה.
- ה-state שלה השתנה. קראתם ל-setter מ-
useStateאו ל-dispatch מ-useReducerעם ערך חדש. - ההורה שלה התרנדר. כברירת מחדל, כשקומפוננטה מתרנדרת, כל קומפוננטה שהיא מחזירה מתרנדרת גם היא, עד הסוף למטה.
- קונטקסט שהיא קוראת השתנה. קומפוננטה שקוראת ל-
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.