useCallback שומר ב-cache פונקציה בין רינדורים: הוא מחזיר את אותו אובייקט פונקציה עד שאחת התלויות שלו משתנה. זה משנה רק כשמשהו משווה את הפונקציה לפי הפניה, ובפועל זה אומר ילד שעטוף ב-memo או אפקט שרושם את הפונקציה כתלות.
שני הכפתורים עם memo ושניהם עושים אותו דבר. החליפו את ערכת הנושא ושימו לב ל-Console: מופיע רק render Inline function, כי saveInline היא פונקציה חדשה בכל רינדור, ולכן memo רואה prop שהשתנה. saveCached היא אותה פונקציה בכל פעם, ולכן מדלגים על הכפתור הזה.
מה הוא שומר
const cachedFn = useCallback(fn, dependencies);
fnהיא הפונקציה שרוצים לשמור. React לא קוראת לה; היא מחזירה אותה לכם.dependenciesמפרט כל ערך מהקומפוננטה ש-fnקוראת. בכל רינדור React משווה אותם לקודמים בעזרתObject.is.- אם שום דבר לא השתנה, מקבלים את הפונקציה מהרינדור הקודם. אם משהו השתנה, מקבלים את הפונקציה שהעברתם הפעם, ו-React שומרת אותה לפעם הבאה.
שימו לב מה זה אומר: פונקציית החץ שבתוך useCallback עדיין נוצרת בכל רינדור, כי היא סתם ארגומנט. useCallback לא חוסך את העבודה הזו. התפקיד היחיד שלו הוא להחזיר פונקציה ישנה וזהה כדי שההפניה תישאר אותה הפניה.
useCallback מול useMemo
זה אותו מנגנון. useMemo שומר ב-cache את התוצאה של קריאה לפונקציה, useCallback שומר את הפונקציה עצמה:
// These two lines are equivalent
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]);
השתמשו ב-useCallback לפונקציות כי זה נקרא טוב יותר, וב-useMemo לערכים מחושבים כמו רשימה מסוננת או אובייקט אפשרויות.
ויתור על תלויות עם פונקציות updater
פונקציה שמורה חייבת לרשום כל ערך state שהיא קוראת, ובכל פעם שה-state הזה משתנה, גם הפונקציה משתנה. ברשימה של שורות עם memo זו בעיה: פונקציית remove שקוראת את todos משתנה בכל פעם ש-todos משתנה, כך שכל שורה שנשארה מתרנדרת שוב אחרי כל הסרה. השתמשו בצורת ה-updater של ה-setter כדי שהפונקציה כבר לא תקרא את todos בכלל.
הסירו שורה: ה-Console שותק, כי שאר השורות קיבלו את אותו אובייקט todo ואת אותה פונקציית remove כמו קודם. שנו את הפונקציה ל-useCallback((id) => setTodos(todos.filter((t) => t.id !== id)), [todos]) ונסו שוב: עכשיו כל שורה שנשארה רושמת רינדור אחרי כל הסרה.
פונקציות כתלויות של אפקט
אפקט שקורא לפונקציה שמוצהרת בקומפוננטה צריך לרשום את הפונקציה הזו כתלות. אם הפונקציה חדשה בכל רינדור, האפקט רץ אחרי כל רינדור. useCallback שומר אותה יציבה עד שהקלטים שלה עצמה משתנים:
הקלדת הודעה לא מחברת מחדש. החלפת חדר רושמת ניתוק מהחדר הישן וחיבור לחדש. מחקו את עטיפת ה-useCallback (השאירו את פונקציית החץ) וכל הקשה מנתקת ומתחברת מחדש.
עם זאת, יש כאן תיקון פשוט יותר: להעביר את הפונקציה לתוך האפקט. אז האפקט תלוי ב-roomId ישירות ואף פונקציה לא צריכה להיות יציבה.
useEffect(() => {
function createOptions() {
return { serverUrl: 'wss://chat.example', roomId };
}
const options = createOptions();
// ...
}, [roomId]);
העדיפו את זה כשהפונקציה משמשת רק את האפקט. שמרו את useCallback לפונקציות שאתם גם מעבירים מטה או מחזירים מהוק מותאם, שבהן מי שקורא להן עשוי לשים אותן במערכי התלויות שלו.
לבדוק אם פונקציה השתנתה
כשילד עם memo ממשיך להתרנדר ואתם לא בטוחים למה, בדקו אם ה-prop של הפונקציה באמת אותו אובייקט בין רינדורים. שמרו את הקודמת ב-ref והשוו:
לחצו על Count: ה-Console אומר increment changed: false, כי step לא השתנה. לחצו על Step והוא אומר true: step היא תלות, ולכן React החזירה את הפונקציה החדשה. פונקציה שמשתנה בכל רינדור בדרך כלל אומרת שתלות מסוימת היא עצמה חדשה בכל רינדור, כמו אובייקט או פונקציה אחרת שמוצהרים בקומפוננטה.
useCallback בהוקים מותאמים
הוק מותאם אישית שמחזיר פונקציות הוא מקום טוב ל-useCallback גם כשלא יודעים מי יקרא לו. הקומפוננטה שמשתמשת בהוק עשויה להעביר את הפונקציות האלה לילד עם memo או לרשום אותן באפקט, והיא לא יכולה להפוך אותן ליציבות מבחוץ. עטיפה שלהן בתוך ההוק נותנת לכל מי שקורא לו הפניה יציבה כברירת מחדל:
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => setCount((c) => c + 1), []);
const reset = useCallback(() => setCount(initial), [initial]);
return { count, increment, reset };
}
הטעות הנפוצה: לעטוף הכל
useCallback סביב handler שהולך ישר ל-<button> או ל-<input> לא עושה שום דבר מועיל. React לא חוסכת שום עבודה באלמנט DOM כי ה-handler שלו נשאר אותו דבר, וקומפוננטה שלא עטופה ב-memo מתרנדרת בכל מקרה כשההורה שלה מתרנדר.
// Pointless: <button> does not care that the function is the same
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>Open</button>;
כל useCallback נוסף מוסיף מערך תלויות שמישהו יכול לטעות בו, ותלות מיושנת היא באג אמיתי (הפונקציה ממשיכה לקרוא state ישן). הוסיפו אותו כשיש ילד עם memo או מערך תלויות בצד המקבל, לא כברירת מחדל.
React Compiler 1.0, כשה-build שלכם משתמש בו, עושה memoization לפונקציות ולערכים אוטומטית, מה שמבטל את הצורך ברוב הקריאות ל-useCallback שנכתבות ידנית. בלי הקומפיילר, הכללים שלמעלה עדיין חלים, ו-useCallback ידני עדיין נפוץ בקוד קיים.
שאלות נפוצות
מה useCallback עושה?
הוא מחזיר את אותו אובייקט פונקציה בכל רינדור עד שאחת התלויות שלו משתנה. הוא לא מונע את יצירת הפונקציה; הוא מחזיר את הפונקציה מרינדור קודם במקום את החדשה.
מתי להשתמש ב-useCallback?
כשמעבירים פונקציה לילד שעטוף ב-memo, או כשהפונקציה היא תלות של אפקט או של הוק אחר. בשני המקרים פונקציה חדשה בכל רינדור הייתה מכשילה את האופטימיזציה או מריצה את האפקט שוב.
מה ההבדל בין useCallback ל-useMemo?
useCallback(fn, deps) שומר ב-cache את fn עצמה. useMemo(() => value, deps) שומר את הערך שפונקציה מחזירה. useCallback(fn, deps) הוא בדיוק useMemo(() => fn, deps).
לעטוף כל פונקציה ב-useCallback?
לא. אם הפונקציה הולכת לאלמנט DOM רגיל או לקומפוננטה שאין לה memo, הפניה יציבה לא משנה כלום, ומערכי התלויות הנוספים הופכים את הקוד לקשה יותר לקריאה וקל יותר לטעות בו.
האם useCallback הופך את הקומפוננטה שלי למהירה יותר?
לא בפני עצמו. הוא עוזר רק כשמשהו בהמשך משווה את הפונקציה לפי הפניה, כמו memo או מערך התלויות של אפקט.