memo מאפשר לקומפוננטה לדלג על רינדור מחדש כשה-props שלה זהים לפעם הקודמת. עטפו קומפוננטה ב-memo(...) וכשההורה שלה מתרנדר, React משווה קודם כל prop חדש לישן; אם כולם שווים, היא משתמשת שוב בתוצאה הקודמת במקום לקרוא לקומפוננטה שוב.
הקלידו בשדה: הוא עומד בקצב, וה-Console מציג render SlowList רק פעם אחת, מהרינדור הראשון. הסירו את עטיפת ה-memo( (ואת הסוגר שלה) והקלידו שוב: כל הקשה מחכה עכשיו 150 מילישניות לרשימה ורושמת רינדור.
התחביר
import { memo } from 'react';
const MemoizedComponent = memo(SomeComponent, arePropsEqual?);
SomeComponentהיא כל קומפוננטת פונקציה. הסגנון הרגיל הוא לעטוף את ההגדרה ישירות:const Card = memo(function Card(props) { ... }). מתן שם לפונקציה הפנימית שומר את השם ב-React DevTools ובהודעות שגיאה.arePropsEqualאופציונלי ולעיתים רחוקות נחוץ (ראו בהמשך).- התוצאה היא קומפוננטה שמרנדרים כמו כל קומפוננטה אחרת.
כברירת מחדל React משווה את ה-props הישנים לחדשים בהשוואה רדודה: היא בודקת כל prop עם Object.is. מחרוזות, מספרים ובוליאנים מושווים לפי ערך, כך ש-title="Hi" שווה ל-title="Hi". אובייקטים, מערכים ופונקציות מושווים לפי הפניה: שני אובייקטים עם אותו תוכן הם עדיין אובייקטים שונים.
בדוגמה שלמעלה, items מוצהר מחוץ לקומפוננטה, ולכן הוא אותו מערך בכל רינדור, וההשוואה עוברת.
אובייקטים ופונקציות חדשים מכשילים את memo
כל דבר שנכתב כ-literal בתוך גוף ההורה נוצר מחדש בכל רינדור: { name: 'Ada' }, [1, 2, 3], () => save(). העבירו אחד כזה לקומפוננטת memo וה-prop אף פעם לא שווה לקודם.
כל לחיצה רושמת render Badge object prop אבל אף פעם לא את גרסת המחרוזת. יש שלוש דרכים לתקן את מקרה האובייקט:
- העבירו ערכים פרימיטיביים.
name="Ada"במקוםuser={{ name: 'Ada' }}כשהילד צריך רק שדה או שניים. - הוציאו קבועים מהקומפוננטה. אובייקט שאף פעם לא משתנה יכול לחיות ברמת המודול, כמו
itemsבדוגמה הראשונה. - שמרו אותו ב-cache. עטפו אובייקטים ומערכים שנבנים מ-props או מ-state ב-useMemo, ופונקציות ב-useCallback.
ל-children יש אותה בעיה. <Card><p>Hello</p></Card> יוצר אובייקט אלמנט <p> חדש בכל רינדור, ולכן Card עם memo שמקבלת children מתרנדרת מחדש בכל פעם.
מה memo לא עוצר
memo מסתכל רק על props שמגיעים מההורה. קומפוננטה עם memo עדיין מתרנדרת כש:
- ה-state שלה עצמה משתנה, דרך
useStateאוuseReducerמשלה; - קונטקסט שהיא קוראת משתנה, דרך
useContext.
לחצו על Unrelated ומדלגים על Panel. החליפו ערכת נושא והיא מתרנדרת, כי היא קוראת את הקונטקסט. לחצו על Like והיא מתרנדרת, כי ה-state שלה השתנה. זו ההתנהגות המכוונת: memo נועד לדלג על עבודה כששום דבר שהקומפוננטה תלויה בו לא השתנה, וקונטקסט ו-state הם דברים שהיא תלויה בהם.
פונקציית השוואה מותאמת
הארגומנט השני מחליף את ההשוואה הרדודה. הוא מקבל את ה-props הקודמים והבאים ומחזיר true כשצריך להתייחס אליהם כשווים, כלומר React מדלגת על הרינדור.
const MemoChart = memo(Chart, (prev, next) => {
return (
prev.points.length === next.points.length &&
prev.points.every((p, i) => p === next.points[i]) &&
prev.color === next.color &&
prev.onSelect === next.onSelect
);
});
לעיתים רחוקות צריך את זה, וקל לטעות בזה. אם הפונקציה מתעלמת מ-prop, הקומפוננטה ממשיכה להציג את הערך הישן של ה-prop הזה. התעלמות מ-prop של פונקציה היא המקרה הגרוע ביותר: הקומפוננטה שדילגו עליה ממשיכה לקרוא לפונקציה הישנה, שקוראת state ישן. השוו כל prop, ואל תעשו בדיקות שוויון עמוקות על נתונים גדולים, כי אז ההשוואה עולה יותר מהרינדור שהיא חוסכת. props יציבים דרך useMemo ו-useCallback הם כמעט תמיד התיקון הטוב יותר.
לבדוק ש-memo עובד
הבדיקה המהירה ביותר היא זו שמשמשת בדף הזה: console.log בראש הקומפוננטה. אם הוא מדפיס כשציפיתם לדילוג, רשמו גם את ה-props, והשוו אותם לקודמים כדי למצוא את זה שהשתנה. בפרויקט אמיתי, React DevTools עוזר עוד יותר: הפעילו בהגדרות שלו את "Highlight updates when components render" כדי לראות אילו קומפוננטות מהבהבות בכל אינטראקציה, והטאב Profiler מקליט כל רינדור, ועם ההגדרה המתאימה דולקת, גם למה כל קומפוננטה התרנדרה.
שני דברים נראים כמו כישלון של memo אבל הם לא:
- הרינדור הראשון תמיד קורה.
memoמדלג רק על רינדורים אחרי שהקומפוננטה התרנדרה פעם אחת עם ה-props האלה. - builds של פיתוח עשויים לרנדר יותר. תחת StrictMode, React מרנדרת כל קומפוננטה פעמיים בפיתוח כדי לתפוס קוד לא טהור. התצוגה המקדימה כאן רצה כמו build של פרודקשן, אז כל שורת לוג למעלה פירושה רינדור אמיתי אחד.
מתי להשתמש ב-memo
memo שווה את זה כשקומפוננטה איטית ברינדור וההורה שלה מתרנדר לעיתים קרובות עם אותם props בשבילה: רשימה ארוכה או גרף ליד שדה קלט, סרגל צד בתוך דף שמתעדכן כל שנייה. לקומפוננטות שמתרנדרות מהר, או שה-props שלהן משתנים בכל מקרה בכל פעם, הוא מוסיף השוואה ולא קונה כלום. לפני memoization, בדקו אם אפשר לארגן מחדש במקום, על ידי הורדת state למטה או העברת JSX בתור children, כמו שמוצג בדף על רינדור.
React Compiler 1.0, אם הפרויקט שלכם משתמש בו, עושה memoization לקומפוננטות ול-props שלהן אוטומטית בזמן ה-build, מה שהופך את רוב הקריאות הידניות ל-memo למיותרות. בלעדיו, memo עובד בדיוק כמו שמתואר כאן ונשאר נפוץ בקוד React.
שאלות נפוצות
מה React.memo עושה?
הוא מחזיר גרסה של קומפוננטה עם memoization. כשההורה מתרנדר, React משווה את ה-props החדשים לקודמים ומדלגת על רינדור הקומפוננטה אם כל prop שווה לפי Object.is.
למה קומפוננטת ה-memo שלי עדיין מתרנדרת מחדש?
בדרך כלל כי prop מסוים הוא אובייקט, מערך או פונקציה חדשים שנוצרו בזמן הרינדור של ההורה, ולכן הוא אף פעם לא שווה לקודם. הקומפוננטה גם מתרנדרת כשה-state שלה עצמה משתנה או כשקונטקסט שהיא קוראת משתנה, עם memo או בלי.
מה ההבדל בין React.memo ל-useMemo?
memo עוטף קומפוננטה ומדלג על הרינדור שלה. useMemo הוא הוק בתוך קומפוננטה ששומר ב-cache ערך מחושב. לעיתים קרובות הם עובדים יחד: useMemo שומר prop של אובייקט יציב כדי ש-memo יוכל לדלג.
לעטוף כל קומפוננטה ב-memo?
לא. רוב הרינדורים זולים, ולהשוואת ה-props יש עלות משלה. השתמשו בו לקומפוננטות שאיטיות ברינדור ושמקבלות לעיתים קרובות את אותם props.
האם memo עובד עם children?
לעיתים רחוקות. JSX שמועבר בתור children הוא אובייקט חדש בכל רינדור, ולכן קומפוננטת memo שמקבלת children כמעט תמיד מתרנדרת מחדש.