Menu

useDeferredValue ב-React: לתת לחלקים איטיים לפגר

useDeferredValue נותן לכם עותק של ערך שמותר לו לפגר מאחור בזמן רינדורים כבדים, כך ששדה קלט נשאר רספונסיבי בזמן שרשימה איטית משיגה את הקצב. למדו איך זה עובד, את הערך ההתחלתי של React 19, ואיך זה משתווה ל-useTransition ול-debounce.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

useDeferredValue נותן לכם עותק של ערך שמותר לו לפגר מאחור. כשהערך משתנה, React מציירת קודם את המסך מחדש עם העותק הישן, מה ששומר על הקלדה מיידית, ואז מרנדרת את החלק האיטי עם הערך החדש ברקע.

הקלידו מילה מהר. השדה עומד בקצב והרשימה מתעמעמת כל עוד היא מאחור. ה-Console רושם שורת SlowList rendering אחת לכל אות, כי SlowList רושמת ברגע ש-React מתחילה לרנדר אותה, אבל הרשימות של ערכי הביניים אף פעם לא מגיעות למסך: כל אות חדשה קטעה את הרינדור ברקע, ו-React התחילה מחדש עם הטקסט העדכני. שנו את query={deferredQuery} ל-query={query} והקלידו שוב: כל הקשה מחכה עכשיו רבע שנייה לרשימה.

התחביר

const deferredValue = useDeferredValue(value);
const deferredValue = useDeferredValue(value, initialValue); // React 19
  • value הוא כל דבר שמרנדרים: מחרוזת משדה קלט, מספר, אובייקט. ערכים פרימיטיביים עובדים הכי טוב, כי אובייקט חדש בכל רינדור תמיד נחשב לשינוי.
  • ערך ההחזרה שווה ל-value רוב הזמן. מיד אחרי ש-value משתנה, הוא עדיין מחזיק את הערך הקודם לרינדור אחד, ומשיג את הקצב ברינדור ברקע.

איך זה עובד

כל שינוי מייצר שני רינדורים:

  1. רינדור דחוף עם ה-value החדש וה-deferredValue הישן. השדה מציג את הטקסט החדש. קומפוננטות שמקבלות רק את deferredValue מקבלות את אותם props כמו קודם.
  2. רינדור ברקע עם ה-deferredValue החדש. זה האיטי. React מרנדרת אותו בפרוסות ונוטשת אותו אם value משתנה שוב לפני שהוא מסתיים, ואז מתחילה מחדש עם הערך העדכני.

אפשר לראות את זה קורה על ידי רישום שני הערכים:

הקלידו אות אחת, נגיד a, וה-Console מדפיס עוד שתי שורות: קודם text = "a" עם deferred = "", ואז שניהם "a". הרינדור הראשון הזה הוא מה ששומר על הקלדה מהירה, אבל רק אם החלק האיטי יכול לדלג עליו. לכן SlowList בדוגמה הראשונה עטופה ב-memo: בזמן הרינדור הדחוף ה-prop query שלה לא השתנה, ולכן React מדלגת עליה. בלי memo, הרינדור הדחוף עדיין היה מריץ את כל הרשימה האיטית, רק עם הטקסט הישן, ולא הייתם מרוויחים כלום.

הצגת תוכן ישן

בזמן שהרינדור ברקע מתבצע, value והערך הדחוי שונים. השוו ביניהם כדי לומר למשתמש שהתוכן לא מעודכן, בלי להסתיר אותו:

const isStale = query !== deferredQuery;

<div style={{ opacity: isStale ? 0.5 : 1, transition: 'opacity 0.2s' }}>
    <SlowList query={deferredQuery} />
</div>

זה בדרך כלל נעים יותר מספינר: התוצאות הישנות נשארות קריאות ומשתנות במקום כשהחדשות מוכנות. אותו רעיון עובד עם Suspense: אם הערך הדחוי גורם לקומפוננטה לעשות suspend בזמן שהיא טוענת נתונים, React ממשיכה להציג את התוכן הישן במקום ה-fallback עד שהתוכן החדש מוכן (ראו Suspense ו-lazy).

הערך ההתחלתי (React 19)

כברירת מחדל לרינדור הראשון אין אחרי מה לפגר, ולכן הערך הדחוי הוא הערך האמיתי מההתחלה. מאז React 19 אפשר להעביר ארגומנט שני. ברינדור הראשון ההוק מחזיר את initialValue, ואז React מרנדרת שוב ברקע עם הערך האמיתי. זה מאפשר להציג משהו זול מיד ולמלא את החלק היקר מיד אחר כך.

ה-Console מציג שני רינדורים: קודם עם "", ואז עם הטקסט המלא. הסירו את הארגומנט השני ונרשם רק רינדור אחד, עם הטקסט המלא מיד.

useDeferredValue מול useTransition

שניהם מסמנים עבודה כלא דחופה ומשתמשים באותו מנגנון רינדור. ההבדל הוא איפה שמים אותם:

  • useTransition עוטף את העדכון. קוראים ל-startTransition(() => setQuery(text)), ולכן צריך את ה-setter. מקבלים את isPending בחינם.
  • useDeferredValue עוטף את הערך. קוראים לו במקום שבו משתמשים בערך, ולכן הוא עובד כשהערך מגיע כ-prop או מגיע מהוק שלא בשליטתכם. את ה-"pending" מחשבים בעצמכם עם value !== deferredValue.
// You own the state: either works, a transition is direct
startTransition(() => setQuery(nextQuery));

// You only receive the value
function SearchResults({ query }) {
    const deferredQuery = useDeferredValue(query);
    return <SlowList query={deferredQuery} />;
}

useDeferredValue מול debounce ו-throttle

debounce מחכה זמן קבוע אחרי השינוי האחרון; throttle מעדכן לכל היותר פעם אחת בכל מרווח. שניהם בוחרים השהיה מראש, כך שבמחשב נייד מהיר הם הופכים את הדף לאיטי יותר ממה שצריך, ובטלפון איטי ההשהיה עשויה עדיין להיות קצרה מדי.

ל-useDeferredValue אין השהיה. הרינדור ברקע מתחיל מיד, כך שבמכשיר מהיר הרשימה מתעדכנת כמעט מיד, ובמכשיר איטי React שומרת על השדה רספונסיבי ומרנדרת את הרשימה כמה שיותר. העבודה ברקע גם ניתנת לקטיעה, ורינדור עם debounce לא: ברגע שרינדור כזה מתחיל, הוא חוסם עד שהוא מסתיים.

מה שהוא לא עושה זה לצמצם בקשות רשת. אם כל ערך חדש מתחיל fetch, הערך הדחוי עדיין משתנה עם כל רינדור שמסתיים, אז עשו debounce לבקשה עצמה כשכמות הבקשות חשובה.

טעויות נפוצות

  • לשכוח את memo. כמו שמוצג למעלה, הקומפוננטה האיטית חייבת להיות מסוגלת לדלג על הרינדור הדחוף. אם היא לא עטופה ב-memo, או שאחד מה-props האחרים שלה הוא אובייקט חדש בכל רינדור, היא מתרנדרת פעמיים בכל הקשה במקום פעם אחת.
  • להעביר אובייקט חדש. useDeferredValue({ query }) יוצר אובייקט חדש בכל רינדור, כך שהערך הדחוי תמיד נראה כאילו השתנה ו-React מתחילה רינדור נוסף ברקע בכל פעם. דחו את הערך הפרימיטיבי (useDeferredValue(query)) ובנו ממנו אובייקטים.
  • לצפות לפחות בקשות. קומפוננטה שטוענת נתונים לפי הערך הדחוי עדיין טוענת עבור כל ערך שמסיים להתרנדר.

מתי לפנות אליו

השתמשו ב-useDeferredValue כשקלט אחד מניע חלק בדף שאיטי ברינדור, אפשר לעשות memo לחלק האיטי, ולא מפריע לכם להציג לרגע תוצאות קצת ישנות: תוצאות חיפוש, גרף שמצטייר מחדש מפילטר, תצוגה מקדימה של טקסט מעוצב. אם החלק האיטי איטי מסיבה שאפשר לתקן, כמו רינדור של 5,000 שורות כשרק 50 גלויות, תקנו את זה קודם. לגרסה של הדפוס הזה שבה ה-setter שלכם, ראו useTransition.

שאלות נפוצות

מה useDeferredValue עושה?

הוא מחזיר גרסה של ערך שעשויה לפגר אחרי הערך האמיתי. כשהערך משתנה, React מרנדרת קודם עם הערך הדחוי הישן, ואז מרנדרת שוב ברקע עם החדש, ואפשר לקטוע את הרינדור ברקע הזה.

למה useDeferredValue לא הופך את הרשימה שלי למהירה יותר?

הקומפוננטה האיטית חייבת להיות עטופה ב-memo ולקבל את הערך הדחוי. אחרת הרינדור הדחוף עדיין מרנדר את הקומפוננטה האיטית, רק עם הערך הישן, וההקלדה נשארת איטית.

מה ההבדל בין useDeferredValue ל-useTransition?

useTransition עוטף את עדכון ה-state, ולכן צריך גישה ל-setter. useDeferredValue עוטף ערך, ולכן הוא עובד כשמקבלים רק את הערך, למשל כ-prop או מהוק שלא בשליטתכם.

האם useDeferredValue הוא debounce?

לא. אין השהיה קבועה. React מתחילה את הרינדור הדחוי מיד אחרי הדחוף, ובמכשיר מהיר הפיגור כמעט לא נראה. הוא גם לא מפחית בקשות רשת בעצמו.

מה הארגומנט השני של useDeferredValue?

מאז React 19 אפשר להעביר initialValue. ברינדור הראשון ההוק מחזיר אותו, ואז React מרנדרת שוב ברקע עם הערך האמיתי.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל