ניקוי אחרי Ref Callback בריאקט
פונקציות Ref Callback יכולות להיות תחליף טוב ויעיל ל useEffect, וכמו useEffect גם להן יש כמה פינות חדות. בואו נראה דוגמה ומה יכול להשתבש.
1. תזכורת: ResizeObserver
ה API בדוגמה זו נקרא ResizeObserver. זה מנגנון שמאפשר לנו לקבל אירועים כל פעם שגודל של אלמנט ב DOM משתנה, אם כתוצאה משינוי גודל חלון או בגלל שינוי ב CSS או כל סיבה אחרת. ה ResizeObserver מקבל את האלמנט ופונקציית טיפול ויפעיל את הפונקציה כל פעם שהגודל משתנה.
אנחנו משתמשים ב ResizeObserver באופן הבא:
- יוצרים ResizeObserver חדש עם הפקודה:
const observer = new ResizeObserver(...)
- בתור פרמטר ל ResizeObserver אנחנו מעבירים פונקציית טיפול בשינוי גודל. הפונקציה מקבלת כקלט מערך של פרטי מידע על אלמנטים שה Observer הסתכל עליהם וגודלם השתנה. לכל פריט מידע יש שדה בשם
contentRect
שמכיל את הגודל שלו:
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`Element resized to: ${width} × ${height}`);
}
});
- בשביל להתחיל להקשיב לשינויי גודל אנחנו קוראים לפונקציה
observe
ומעבירים לה אלמנט ב DOM אליו אנחנו רוצים להאזין:
observer.observe(targetElement);
- אחרי שאנחנו מסיימים להאזין עלינו להפעיל disconnect כדי לנקות את הזיכרון ולהפסיק לקרוא לפונקציה כשגודל האלמנט משתנה:
observer.disconnect();
2. חיבור ResizeObserver לריאקט
בשביל לחבר ResizeObserver לקומפוננטה בריאקט נוכל להשתמש ב Ref Callback. בעת יצירת הקומפוננטה נבנה את ה ResizeObserver, נגדיר פונקציית טיפול בשינוי גודל שתכתוב את גודל האלמנט לתוך משתנה סטייט ונציג את תוכן משתנה הסטייט. זה הקוד:
'use client';
import { useState } from "react";
export default function MeasureExample() {
const [height, setHeight] = useState(0)
const measuredRef = (node: HTMLElement|null) => {
if (!node) { throw new Error("Should not be here")}
// only one entry - because we only "observe" one node
const observer = new ResizeObserver(([entry]) => {
setHeight(entry.contentRect.height)
})
observer.observe(node)
return () => {
observer.disconnect()
}
}
return (
<div
ref={measuredRef}
className="h-screen bg-amber-600"
>
I am {Math.round(height)}px tall
</div>
)
}
שימו לב שפונקציית ה Ref Callback מחזירה פונקציה. מבנה זה של פונקציית Ref Callback שמחזירה פונקציה אומר לריאקט שאנחנו צריכים להריץ קוד ניקוי אם במקרה הקומפוננטה תעזוב את המסך או אם ה div שעליו ה Ref Callback הזה מחובר יצא מה DOM. בכל מקרה של Ref Callback שיוצר חיבור ארוך טווח עם אוביקט חיצוני עלינו לדאוג לנקות אחרינו.
3. למה האלמנט יכול להיות null?
החתימה של הפונקציה מבלבלת - הפונקציה יכולה לקבל אלמנט DOM או null, אבל בתחילת הפונקציה אני מוודא שלא קיבלתי null. הסיבה היא שהחתימה של Callback Ref השתנתה בגירסה 19 של ריאקט. עד גירסה 19 לא היתה אפשרות להחזיר פונקציית ניקוי והיינו צריכים בקוד ה Callback Ref לבדוק אם קיבלנו null ואם כן לנקות את החיבור. בריאקט 19 נכנסה האפשרות להחזיר פונקציית ניקוי, וכשמחזירים פונקציית ניקוי ריאקט לא יפעיל את ה Callback Ref עם null ובמקום זה יקרא לפונקציית הניקוי. כתיב פונקציית הניקוי הרבה יותר נוח כי הוא מאפשר להשתמש באותו משתנה observer שכבר הגדרתי בפונקציה כדי לבטל את החיבור.
4. כמה פעמים הפונקציה מופעלת? מתי מופעל הניקוי?
הקוד עובד אבל כולל רמאות. בואו נוסיף הודעות הדפסה כדי לראות אותה:
'use client';
import { useState } from "react";
export default function MeasureExample() {
const [height, setHeight] = useState(0)
const measuredRef = (node: HTMLElement|null) => {
if (!node) { throw new Error("Should not be here")}
console.log(`Creating the Observer`);
// only one entry - because we only "observe" one node
const observer = new ResizeObserver(([entry]) => {
setHeight(entry.contentRect.height)
})
observer.observe(node)
return () => {
console.log(`Disconnect`);
observer.disconnect()
}
}
return (
<div
ref={measuredRef}
className="h-screen bg-amber-600"
>
I am {Math.round(height)}px tall
</div>
)
}
שימו לב לשתי תופעות מעניינות:
בעליית העמוד אני מקבל את ההודעה Creating the Observer, אחריה Disconnect ואחריה שוב Creating the Observer.
אחרי כל שינוי גודל אני מקבל הודעת Disconnect ואז שוב Creating the Observer.
הסיפור הראשון פחות מטריד - במצב פיתוח ריאקט "בועט" קצת בקוד שלנו כדי ששגיאות קטנות יהפכו בולטות יותר. מבחינת ריאקט בסוף Callback Ref חייב להופיע קוד ניקוי ובשביל לבדוק את זה הוא מפעיל את ה Callback Ref ואז מנקה ואז מפעיל עוד פעם רק בשביל לראות שהכל עובד ולא שכחנו כלום.
אבל למה החיבור נוצר מחדש אחרי כל שינוי גודל?
5. שימוש ב useCallback כדי לא ליצור את האפקט מחדש אחרי כל שינוי
שימו לב ל JSX הבא:
<div
ref={measuredRef}
className="h-screen bg-amber-600"
>
אנחנו יוצרים div ומעבירים לו שני דברים, את ה ref callback ומחרוזת בתור className. עכשיו צריך לספר משהו על ריאקט, אלמנטים ב DOM ומאפיין ref: אם ה Callback Ref משתנה ב Render, כלומר ריאקט מפעיל את פונקציית הקומפוננטה, מקבל JSX עם Callback Ref שונה ממה שהיה פעם קודמת שהוא הפעיל את פונקציית הקומפוננטה, אז ריאקט ינתק את האלמנט מהמסך, יפעיל את קוד הניקוי של ה Callback Ref ששמור לו ויריץ את ה Callback Ref החדש.
אבל רגע, מה פתאום "משנים את ה Callback Ref" אתם שואלים, הרי אנחנו רואים שזה תמיד אותה פונקציה, measureRef. נכון?
לא מדויק. כשריאקט מפעיל את פונקציית הקומפוננטה MeasureExample הוא מבצע את כל הפקודות בתוך הפונקציה. הפקודה:
const measureRef = ( ... ) => { ... };
יוצרת פונקציה אנונימית חדשה ומגדירה את המשתנה measureRef שיצביע על אותה פונקציה. בפעם הבאה שיהיה render, תיווצר שוב פונקציה אנונימית חדשה שתישמר לתוך המשתנה measureRef ותעבור להיות ה ref החדש של ה div. נכון, הפונקציה החדשה תהיה ממש זהה לפונקציה הקודמת, אבל זה לא הופך אותה לפחות חדשה. אתם יכולים לעשות את הניסוי הבא בשביל לראות את התופעה:
const a = () => 2;
const b = () => 2;
console.log(a == b)
ותגלו שהמשתנים לא שווים - בכל משתנה יש פונקציה אנונימית אחרת שבמקרה שתי הפונקציות עושות בדיוק אותו דבר ומחזירות 2.
בשביל שריאקט לא יחשוב שאנחנו "מחליפים" את ה Callback Ref עלינו לשמור את הפונקציה באיזשהו מקום מחבוא כך שכל פעם שיהיה Render נוסף ניקח אותה מהמחבוא ונשתמש בה ולא נייצר אחת חדשה. לריאקט יש מנגנון מובנה לשמור פונקציות בצד בדיוק למצבים האלה וזה נקרא useCallback
.
פונקציית useCallback
של ריאקט מקבלת פונקציה אחרת ורשימה של דברים שאם הם משתנים צריך להחליף את הפונקציה. כל עוד הדברים ברשימה שהעברנו בפרמטר השני לא השתנו useCallback יחזיר בדיוק את אותה פונקציה שהוא קיבל ב render הראשון. הקוד המתוקן עם useCallback
נראה ככה:
'use client';
import { useState, useCallback } from "react";
export default function MeasureExample() {
const [height, setHeight] = useState(0)
const measuredRef = useCallback((node: HTMLElement|null) => {
if (!node) { throw new Error("Should not be here")}
console.log(`Creating the Observer`);
// only one entry - because we only "observe" one node
const observer = new ResizeObserver(([entry]) => {
setHeight(entry.contentRect.height)
})
observer.observe(node)
return () => {
console.log(`Disconnect`);
observer.disconnect()
}
}, [setHeight]);
return (
<div
ref={measuredRef}
className="h-screen bg-amber-600"
>
I am {Math.round(height)}px tall
</div>
)
}
שימו לב שרשמתי את setHeight
ברשימת התלויות של useCallback. הפונקציה הפנימית משתמשת ב setHeight ולכן אם setHeight תשתנה יש ליצור מחדש את הפונקציה הפנימית. אבל אתם יכולים להיות רגועים, פונקציית setter שחוזרת מ useState תמיד תישאר זהה.
6. וניסוי למתקדמים
עדכנו את הקוד והפכו את ה Ref Callback לכתיב useEffect. מה ההבדל בין השניים? מתי תעדיפו להשתמש בכל כתיב?