useRef, useMemo ו-useCallback מוסברים¶
אחרי ש-useState ו-useEffect כבר מוכרים, מגיעים שלושה הוקים שנראים דומים אבל פותרים בעיות שונות לגמרי - useRef, useMemo ו-useCallback. הרבה מפתחים משתמשים בהם "כי ראו בקוד של מישהו אחר", בלי להבין באמת מתי הם נחוצים. בואו נעשה סדר.
useRef - ערך שנשמר בין רינדורים, בלי לגרום לרינדור¶
useRef יוצר "קופסה" שערך שלה נשמר לאורך חיי הקומפוננטה, בדיוק כמו state. אבל בניגוד ל-useState, שינוי ערך של ref לא גורם לריאקט לרנדר מחדש את הקומפוננטה.
function Timer() {
const countRef = useRef(0);
function increment() {
countRef.current += 1;
console.log(countRef.current); // מתעדכן, אבל הממשק לא מתרנדר מחדש
}
return <button onClick={increment}>הוסף</button>;
}
השימוש הנפוץ ביותר ל-useRef הוא גישה ישירה לאלמנט DOM, למשל למיקוד (focus) אוטומטי בשדה קלט:
function SearchBox() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
שימוש נוסף הוא שמירת ערך "שקורה" שלא אמור להשפיע על מה שמוצג - למשל מזהה של טיימר שרוצים לבטל מאוחר יותר, או ערך קודם לצורך השוואה, בלי להצדיק רינדור מחדש בכל שינוי.
useMemo - חישוב שנשמר בזיכרון בין רינדורים¶
useMemo פותר בעיה שונה לגמרי - חישוב יקר שלא צריך לרוץ מחדש בכל רינדור, אלא רק כשהערכים שהוא תלוי בהם באמת משתנים.
function ProductList({ products, filterText }) {
const filteredProducts = useMemo(() => {
console.log("מסנן מחדש...");
return products.filter((p) => p.name.includes(filterText));
}, [products, filterText]);
return (
<ul>
{filteredProducts.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
בלי useMemo, הסינון היה רץ מחדש בכל רינדור, גם אם products ו-filterText לא השתנו כלל, למשל אם הקומפוננטה מתרנדרת מחדש בגלל סטייט אחר לגמרי שקשור אליה. useMemo "זוכר" את התוצאה, ומחשב מחדש רק כשמערך התלויות שלו (בדיוק כמו ב-useEffect) משתנה.
useCallback - פונקציה שנשמרת בזיכרון¶
useCallback דומה מאוד ל-useMemo, אבל במקום לזכור תוצאה של חישוב, הוא זוכר את הפונקציה עצמה, כדי שלא תיווצר מחדש בכל רינדור:
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("נלחץ");
}, []);
return <ExpensiveChild onClick={handleClick} />;
}
בלי useCallback, בכל רינדור מחדש של ParentComponent, handleClick הייתה פונקציה חדשה לגמרי מבחינת ג'אווהסקריפט, גם אם היא עושה בדיוק אותו דבר. זה משנה כי ExpensiveChild, אם היא עטופה ב-React.memo, עלולה לחשוב ש-prop חדש הגיע ולרנדר מחדש שלא לצורך, רק כי onClick "נראה" שונה מבחינת השוואת reference.
מתי בכלל צריך את שני האלה - ולמה לא תמיד¶
זו הנקודה הכי חשובה, ואולי הכי לא אינטואיטיבית - useMemo ו-useCallback הם כלי אופטימיזציה, לא כלי בסיס. יש להם עלות משלהם - ריאקט צריך לזכור את הערך הקודם ולהשוות מערך תלויות בכל רינדור, מה שגם עולה זמן וזיכרון. בקומפוננטה פשוטה, בלי חישוב יקר אמיתי ובלי React.memo בילדים, השימוש בהם לא רק מיותר - הוא לפעמים אפילו מאט את הקוד, ותמיד מוסיף מורכבות שקשה יותר לקרוא.
כלל אצבע טוב - כתבו קוד בלי useMemo ו-useCallback קודם. הוסיפו אותם רק אחרי שזיהיתם בעיית ביצועים אמיתית, לרוב עם כלי מדידה כמו React DevTools Profiler, לא "כי אולי זה יעזור".
// לרוב מיותר בקומפוננטה פשוטה
const total = useMemo(() => a + b, [a, b]);
// לרוב מספיק בלי useMemo בכלל
const total = a + b;
טבלת הכרעה מהירה¶
- צריך לגשת ישירות ל-DOM, או לשמור ערך שלא אמור לגרום לרינדור? -
useRef. - יש חישוב שבאמת יקר (מיון מערך ענק, חישוב מורכב) ולא רוצים שירוץ בכל רינדור? -
useMemo. - מעבירים פונקציה כ-prop לקומפוננטת ילד שעטופה ב-React.memo, ורוצים למנוע רינדור מיותר שלה? -
useCallback. - אף אחד מהמצבים האלה לא מתקיים? - כנראה שלא צריך אף אחד מהשלושה.
למה זה נושא שכדאי להבין לעומק, לא רק לשנן¶
בראיונות עבודה לתפקידי ריאקט, שאלה על ההבדל בין ההוקים האלה נפוצה מאוד, בדיוק כי היא בודקת אם המועמד באמת מבין איך ריאקט מרנדר מחדש, ולא רק יודע לשנן "מתי להשתמש במה". להבין את זה לעומק גם עוזר בזיהוי ותיקון בעיות ביצועים אמיתיות בפרויקטים גדולים.
בקורס צד לקוח יש פרק שמדגים בעיית ביצועים אמיתית, ואז פותר אותה עם ההוקים האלה בדיוק כדי להראות מתי הם עוזרים ומתי הם מיותרים.
לסיכום¶
useRef שומר ערך בין רינדורים בלי לגרום לרינדור מחדש, useMemo זוכר תוצאה של חישוב יקר, ו-useCallback זוכר פונקציה כדי שלא תיווצר מחדש בכל רינדור. שלושתם כלי אופטימיזציה שכדאי להוסיף רק כשיש בעיה אמיתית, לא כברירת מחדל בכל קומפוננטה.
לא בטוחים אם אתם צריכים useMemo במקרה שלכם? תשאלו בדיסקורד.