useMemo שומר ב-cache את התוצאה של חישוב בין רינדורים. React מריצה את הפונקציה שלכם ברינדור הראשון, שומרת את התוצאה, ומחזירה את אותה תוצאה ברינדורים הבאים עד שאחד הערכים במערך התלויות משתנה.
פתחו את ה-Console והחליפו את ערכת הנושא כמה פעמים: שום דבר לא נרשם, וההחלפה מרגישה מיידית. החליפו טאבים ו-filtering for active מופיע, עם השהיה קצרה, כי tab היא תלות. עכשיו החליפו את שורת ה-useMemo ב-const visible = filterTodos(todos, tab); והחליפו שוב את ערכת הנושא: כל לחיצה רושמת ונתקעת ל-100 מילישניות.
התחביר
const cachedValue = useMemo(calculateValue, dependencies);
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
calculateValueהיא פונקציה בלי ארגומנטים שמחזירה את הערך שרוצים לשמור. React קוראת לה בזמן הרינדור.dependenciesהיא רשימת כל הערכים מהקומפוננטה שהפונקציה קוראת: props, state, ומשתנים שמוצהרים בגוף הקומפוננטה.- בכל רינדור React משווה כל תלות לערך שלה מהרינדור הקודם בעזרת
Object.is. אם כולם תואמים, היא מחזירה את הערך השמור בלי לקרוא לפונקציה. אם אחד שונה, היא קוראת לפונקציה ושומרת את התוצאה החדשה.
useMemo הוא הוק, ולכן הוא עוקב אחרי כללי ההוקים: קוראים לו ברמה העליונה של הקומפוננטה, לא בתוך לולאה או תנאי.
התלויות חייבות להיות שלמות
השמיטו ערך מהמערך וה-cache ממשיך להגיש תוצאה שחושבה מהערך הישן שלו. כאן הסכום קורא את qty אבל רושם רק את price:
לחצו על Quantity: הוא עולה ל-2, אבל הסכום נשאר 10 ושום דבר לא נרשם. לחצו על Price והסכום קופץ ל-22, כי רק אז החישוב רץ שוב, והפעם קורא את ה-qty הנוכחי. הוסיפו את qty למערך ושני הכפתורים מעדכנים את הסכום. כלל ה-lint react-hooks/exhaustive-deps תופס את הטעות הזו בפרויקט אמיתי.
הטעות ההפוכה היא תלות שחדשה בכל רינדור, כמו אובייקט שנוצר בגוף הקומפוננטה. היא אף פעם לא תואמת לערך הקודם שלה, ולכן ה-cache אף פעם לא משמש:
const filters = { tab, query }; // a new object every render
const visible = useMemo(() => applyFilters(todos, filters), [todos, filters]); // recomputes every time
// List the primitive values instead
const visible = useMemo(() => applyFilters(todos, { tab, query }), [todos, tab, query]);
שמירה על אובייקט יציב בשביל ילד עם memo
הסיבה השנייה לפנות ל-useMemo לא קשורה בכלל לחישובים איטיים. ילד שעטוף ב-memo מדלג על הרינדור שלו רק כשכל prop זהה לפעם הקודמת. אובייקט literal שנכתב בגוף הקומפוננטה הוא אובייקט חדש בכל רינדור, ולכן הוא מכשיל את memo גם כשהתוכן שלו לא השתנה. useMemo שומר את אותו אובייקט עד שהקלטים שלו משתנים.
הקלידו בשדה ההערה ו-Chart שותקת, ואילו Switch unit רושם רינדור אחד. החליפו את הקריאה ל-useMemo ב-const options = { unit, max: unit === 'km' ? 100 : 62 }; רגיל וכל הקשה בהערה רושמת render Chart.
אותו דבר חל על אפקט שתלוי באובייקט. אם אפקט רושם את options בתלויות שלו ו-options נבנה מחדש בכל רינדור, האפקט רץ אחרי כל רינדור. useMemo מתקן את זה, אבל התיקון הפשוט יותר הוא לעיתים קרובות ליצור את האובייקט בתוך האפקט ולהיות תלויים בערכים הפרימיטיביים:
useEffect(() => {
const options = { serverUrl, roomId };
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
מתי לא להשתמש ב-useMemo
רוב החישובים בקומפוננטה זולים: חיבור מספרים, שרשור שתי מחרוזות, סינון כמה עשרות פריטים. שמירה שלהם ב-cache חוסכת כמעט כלום: React עדיין שומרת את הערך ומשווה את התלויות בכל רינדור, והקוד נהיה קשה יותר לקריאה.
// No useMemo needed
const fullName = first + ' ' + last;
const doneCount = todos.filter((t) => t.done).length;
כדי לגלות אם משהו איטי, מדדו אותו. עטפו את החישוב ב-console.time('filter') וב-console.timeEnd('filter'), הריצו את האינטראקציה, והסתכלו על המספר. בתור מדריך גס, עבודה שלוקחת מילישנייה או יותר במחשב שלכם היא מועמדת, בהתחשב בכך שהמכשירים של המשתמשים שלכם עשויים להיות איטיים יותר. לפני שפונים ל-useMemo, בדקו גם אם העבודה בכלל שייכת לרינדור: נתונים שאף פעם לא משתנים אפשר לחשב פעם אחת מחוץ לקומפוננטה, ו-state שרק חלק קטן מהדף משתמש בו יכול לעבור לקומפוננטה קטנה יותר (ראו רינדור).
useMemo מול useEffect לערכים נגזרים
דפוס נפוץ הוא לחשב ערך באפקט ולשמור אותו ב-state:
// Two renders per change, and the first one has stale data
const [visible, setVisible] = useState([]);
useEffect(() => {
setVisible(filterTodos(todos, tab));
}, [todos, tab]);
// One render, always in sync
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
גרסת האפקט מתרנדרת פעם אחת עם ה-visible הישן, אחר כך מריצה את האפקט, קובעת state ומתרנדרת שוב. זה רינדור נוסף, ובראשון הרשימה לא תואמת לטאב. חישוב בזמן הרינדור עם useMemo (או בלי שום הוק, אם העבודה זולה) נותן את הערך הנכון באותו רינדור. אפקטים נועדו לסנכרון עם דברים מחוץ ל-React, לא לגזירת חלק state אחד מאחר.
רמז לביצועים, לא הבטחה
useMemo אומר ל-React שמותר לה להשתמש שוב בתוצאה. הוא לא מבטיח שהיא תעשה את זה. React שומרת את הערך ב-cache בפעולה רגילה, אבל היא יכולה לזרוק אותו, למשל כשעורכים את הקובץ בזמן פיתוח, או כשקומפוננטה עושה suspend בזמן ה-mount הראשון שלה. כתבו את הקוד כך שהוא עדיין נכון אם החישוב רץ שוב: הפונקציה חייבת להיות טהורה, בלי תופעות לוואי כמו בקשות, מנויים או שינוי משתנים מחוצה לה. תופעות לוואי שייכות ל-event handler או לאפקט.
בפיתוח, תחת StrictMode, React קוראת לפונקציית החישוב שלכם פעמיים כדי לעזור לכם לזהות קוד לא טהור. התצוגה המקדימה כאן רצה כמו build של פרודקשן, אז תראו כל לוג של filtering for פעם אחת.
useMemo ו-React Compiler
React Compiler 1.0 הוא כלי build שמנתח את הקומפוננטות שלכם ומוסיף memoization אוטומטית, ושומר ב-cache חישובים ו-JSX במקומות שבהם useMemo, useCallback ו-memo היו עוזרים. בפרויקט שמשתמש בו, תכתבו פחות מאלה ידנית. useMemo ידני עדיין עובד לצד הקומפיילר, הוא עדיין הכלי כשהקומפיילר לא מוגדר, ותפגשו אותו ברוב קוד ה-React הקיים.
שאלות נפוצות
מה useMemo עושה ב-React?
הוא קורא לפונקציית החישוב שלכם ברינדור הראשון, שומר את התוצאה, וברינדורים הבאים מחזיר את התוצאה השמורה כל עוד כל ערך במערך התלויות זהה לפעם הקודמת (בהשוואה עם Object.is).
מתי להשתמש ב-useMemo?
כשחישוב איטי באופן מורגש והקלטים שלו משתנים לעיתים רחוקות, או כשצריך שאובייקט או מערך ישמרו על אותה הפניה בין רינדורים כי ילד עם memo או אפקט תלויים בהם. לעבודה זולה, דלגו עליו.
מה ההבדל בין useMemo ל-useCallback?
useMemo שומר ב-cache את הערך שהפונקציה שלכם מחזירה. useCallback שומר את הפונקציה עצמה. useCallback(fn, deps) זהה ל-useMemo(() => fn, deps).
האם useMemo מבטיח שהערך אף פעם לא יחושב מחדש?
לא. זה רמז לביצועים. React עשויה לזרוק את הערך השמור בחלק מהמקרים, למשל בפיתוח כשעורכים את הקובץ, ולכן הקוד שלכם חייב לעבוד נכון גם אם החישוב רץ שוב.
האם עדיין צריך useMemo עם React Compiler?
React Compiler יכול להוסיף memoization אוטומטית כשה-build שלכם משתמש בו. בלי הקומפיילר, או במקרים שהוא מדלג עליהם, useMemo ידני עדיין עובד באותה צורה ועדיין נפוץ בקוד קיים.