use הוא API של React 19 שקורא ערך מ-promise או מקונטקסט בזמן שקומפוננטה מתרנדרת. use(promise) עוצר את הקומפוננטה עד שה-promise מסתיים, ומציג בינתיים את ה-fallback של ה-<Suspense> הקרוב; use(context) עובד כמו useContext אבל יכול לשבת בתוך if או לולאה.
הפונקציה fetchUser שמתחת מחליפה בקשה אמיתית.
הרינדור הראשון מציג "Loading..." לשנייה אחת, ואז את הפרופיל. לחצו על User 2: ה-fallback חוזר בזמן שה-promise החדש ממתין. ל-Profile אין מצב טעינה משלה; היא כתובה כאילו הנתונים כבר שם.
איך use(promise) עובד
כש-Profile קוראת ל-use(userPromise):
- אם ה-promise הסתיים,
useמחזיר את הערך שלו והרינדור ממשיך. - אם הוא עדיין ממתין, React מפסיקה לרנדר את
Profileומציגה את ה-fallback של ה-<Suspense>הקרוב מעליה. כשה-promise מסתיים, React מרנדרת שוב אתProfile, והפעםuseמחזיר את הערך. - אם הוא נכשל, React מציגה במקום את ה-error boundary הקרוב.
React זוכרת את התוצאה על אובייקט ה-promise עצמו. לכן ה-promise חייב להיות אותו אובייקט ברינדור הבא.
כדי להשאיר את הפרופיל הישן על המסך בזמן שהבא נטען, קבעו את ה-promise בתוך transition. React מחכה אז במקום להציג שוב את ה-fallback:
<button onClick={() => startTransition(() => setUserPromise(fetchUser(2)))}>User 2</button>
איפה ליצור את ה-promise
זה החלק שמכשיל אנשים. כתיבת קריאת ה-fetch בתוך הקומפוננטה שקוראת אותו נראית טבעית ולא עובדת:
// Do not do this
function Profile({ id }) {
const user = use(fetchUser(id)); // a new promise on every render
return <p>{user.name}</p>;
}
כל רינדור קורא ל-fetchUser ומקבל promise חדש וממתין. Profile עושה suspend. כשה-promise הזה מסתיים, React מרנדרת שוב את Profile, מה שיוצר עוד promise ממתין, שעושה suspend שוב. הקומפוננטה אף פעם לא מציגה את הנתונים, וטאב הרשת מתמלא בבקשות.
צרו את ה-promise במקום שלא רץ מחדש בכל רינדור של הקורא:
- בהורה, שמור ב-state (כמו בדוגמה הראשונה) או שנוצר ב-event handler.
- ברמת המודול, כשהנתונים לא תלויים ב-props:
const configPromise = fetchConfig();מעל הקומפוננטה. - ב-cache לפי הארגומנטים, כך שאותו id מחזיר את אותו promise.
- ב-framework: Server Component יכול להעביר promise לקומפוננטת לקוח כ-prop, ו-route loaders עושים את אותו דבר. זה המקרה ש-
useתוכנן בשבילו.
הנה גרסת ה-cache, כך שקומפוננטה יכולה לבקש נתונים לפי id ועדיין לקבל promise יציב:
לחצו על שלוש הערים, ואז חזרו לאחת שכבר פתחתם. הקונסול רושם בקשה אחת לכל עיר, ועיר שכבר ראיתם מופיעה בלי ה-fallback, כי ה-promise שלה כבר הסתיים. מחקו את הבדיקה if (!cache.has(city)) והקומפוננטה חוזרת לעשות suspend לנצח.
cache אמיתי צריך גם דרך לפוגג רשומות. ספריות נתונים ו-frameworks מטפלים בזה, ולכן רוב האפליקציות מקבלות את ה-promises שלהן מאחד כזה.
use(context), אפילו בתוך if
use(SomeContext) מחזיר את אותו ערך כמו useContext(SomeContext). ההבדל הוא איפה אפשר לקרוא לו. כל הוק אחר חייב לרוץ ברמה העליונה, באותו סדר בכל רינדור (כללי ההוקים). את use אפשר לקרוא אחרי return מוקדם, בתוך תנאי, או בלולאה.
לחצו על הכפתור כדי לעבור בין שני הענפים. החליפו את use(UserContext) ב-useContext(UserContext) והקוד שובר את כללי ההוקים, כי ההוק היה רץ בחלק מהרינדורים ובאחרים לא.
גם ל-use יש מגבלות: קראו לו רק בזמן רינדור של קומפוננטה או בתוך הוק מותאם. ב-event handler או באפקט, קראו את הקונטקסט מערך שכבר קיבלתם בזמן הרינדור.
שגיאות: error boundaries, לא try/catch
כשה-promise נכשל, השגיאה הולכת ל-error boundary הקרוב, באותה דרך כמו שגיאת רינדור.
פתחו את Post 2 וה-boundary מציג את הודעת השגיאה. פתחו שוב את Post 1 וזה עובד: key={id} נותן ל-boundary מצב חדש לכל פוסט, כך שהוא לא ממשיך להציג את השגיאה הישנה.
אי אפשר לתפוס use ממתין או שנכשל עם try/catch. בזמן שה-promise ממתין, use קוטע את הרינדור על ידי זריקת ערך מיוחד, ו-catch סביבו היה בולע אותו. הערך שנתפס הוא שגיאה שההודעה שלה מתחילה ב-"Suspense Exception: This is not a real error!", ובפיתוח React גם רושמת "use was called from inside a try/catch block". כדי להציג ערך גיבוי במקום מסך שגיאה, טפלו בכישלון על ה-promise לפני שאתם מעבירים אותו מטה:
const postPromise = fetchPost(id).catch(() => 'This post could not be loaded.');
use בהשוואה לטעינה עם useEffect
הדפוס הקלאסי בצד הלקוח טוען באפקט ומחזיק שלושה חלקי state:
function Profile({ id }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
fetchUser(id).then(
(u) => !ignore && setUser(u),
(e) => !ignore && setError(e),
);
return () => {
ignore = true;
};
}, [id]);
if (error) return <p>Error</p>;
if (!user) return <p>Loading...</p>;
return <p>{user.name}</p>;
}
useEffect | use | |
|---|---|---|
| מתי הבקשה מתחילה | אחרי שהרינדור הראשון על המסך | כשה-promise נוצר, אולי לפני שהקומפוננטה מתרנדרת |
| מצב טעינה | if (!user) משלכם | ה-<Suspense> הקרוב |
| שגיאות | state error משלכם | ה-error boundary הקרוב |
| race conditions | אתם מגינים מפניהן (ignore) | הקומפוננטה תמיד קוראת את ה-promise שקיבלה |
| מי יוצר את ה-promise | הקומפוננטה | הורה, cache או framework |
גרסת האפקט עצמאית, ולכן היא עדיין הגישה הנפוצה ביותר באפליקציות בלי framework; הדף על טעינת נתונים מכסה אותה במלואה. use זורח כשמשהו מעל הקומפוננטה מתחיל את הבקשה מוקדם ומעביר את ה-promise מטה.
שאלות נפוצות
מה ההוק use עושה ב-React?
use(resource) מחזיר את הערך של promise או של קונטקסט. עם promise, הקומפוננטה עושה suspend עד שהוא מסתיים וה-<Suspense> הקרוב מציג את ה-fallback שלו; אם הוא נכשל, ה-error boundary הקרוב מוצג במקום.
אפשר לקרוא ל-use בתוך תנאי?
כן. use הוא ההוק היחיד שאפשר לקרוא לו בתוך משפטי if, בלולאות ואחרי return מוקדם. עדיין צריך לקרוא לו מקומפוננטה או מהוק, אף פעם לא מ-event handler או מאפקט.
למה use(fetch()) בתוך הקומפוננטה שלי נכנס ללולאה אינסופית?
כל רינדור יוצר promise חדש. הקומפוננטה עושה עליו suspend, React מרנדרת שוב כשהוא מסתיים, והרינדור הזה יוצר עוד promise ממתין. צרו את ה-promise מחוץ לרינדור: ב-state של הורה, ברמת המודול, ב-cache, או ב-loader של framework.
אפשר לעטוף את use ב-try/catch?
לא. כש-promise ממתין, use קוטע את הרינדור על ידי זריקה, ותפיסה של זה שוברת אותו. טפלו ב-promise שנכשל עם error boundary, או חברו .catch() ל-promise כדי שיסתיים עם ערך גיבוי.
להחליף טעינת נתונים עם useEffect ב-use?
use צריך promise שנוצר פעם אחת ומשמש שוב, וזה מה ש-frameworks וספריות נתונים נותנים לכם. באפליקציית לקוח רגילה, טעינה באפקט (או עם ספרייה כמו TanStack Query) עדיין נפוצה ובסדר.