קצת רקע:

Suspense זו קומפוננטה ניסיונית ששוחררה כבר בריאקט 16.6, מה שאומר שהיא עובדת כבר עכשיו.
אבל הגרסה הסופית והיציבה שלה צפויה להשתחרר בריאקט 18.

מה זה Suspense?

Suspense זו תבנית שמאפשרת למנגנון של ריאקט לחכות עם רינדור של קומפוננטות לפני שמתקבלים כל הנתונים הדרושים לרינדור (בד"כ שליפת נתונים מserver).
בזמן ההמתנה יוצג placeholder - fallback.

אם את/ה מפתח/ת אפליקצית ריאקט בלי להכיר את Suspense, בטח את/ה חושב/ת לעצמך: הרי הבעיה הזאת לא חדשה, וגם עד היום התמודדתי איתה בהצלחה!
מי לא מכיר/ה את הקלאסיקה הבאה:
קוד:
const User: React.FC = () => {

    const [user, setUser] = useState<any | null>(null);
    const [isLoading, setIsLoading] = useState<boolean>(false);
    const [error, setError] = useState<any | null>(null);

    useEffect(() => {
        setIsLoading(true);
        fetchUser().then((result: any) => setUser(result))
            .catch((e: any) => setError(e))
            .finally(() => setIsLoading(false));
    }, [])

    return <>
        {isLoading && <>Loading...</>}
        {error && <>Error Occured: {error}</>}
        {user && <><span>{user.id}</span>
            <span>{user.name}</span>
        </>}
    </>;
};

export default User;
זהו מבנה קלאסי של קומפוננטה שמציגה למשתמש placeholder בזמן שהטעינה מתבצעת, שידע שקורה משהו.

זה עובד פרפקט, אבל במבט על אפליקציה שלמה יש לזה גם חסרונות:

1. הקומפוננטה צריכה להתעסק בלוגיקה צדדית שלא קשורה לתפקיד העיקרי שלה.
2. לעיתים קרובות אנחנו מעוניינים להציג placholder משותף למספר קומפוננטות, שיוצג עד שכולן ייטענו.
אם נשתמש בשיטה הקלאסית נצטרך לתחזק מסר משתנים בוליאנים שכל אחד מהם ייתן אינדיקציה על פעולה אחת, ולבדוק את הערך של כולם. גם אם נממש את זה בצורה של מערך או רשימה, עדיין זו חתיכת לוגיקה שקשה לקריאה ולכתיבה (מניסיון).


אז איך משתמשים בSuspense?

ככה:
קוד:
        <Suspense fallback={<div>Loading...</div>}>
          <User />
        </Suspense>

הקוד מספר לנו שעד שהקומפוננטה User מקבלת את המידע מהserver, היא לא תרונדר ובמקומה יוצג הטקסט Loading... ממש כמו בשיטה הקלאסית.
אבל לא מספיק לעטוף את User בSuspense. הקומפוננטה User צריכה לדעת לספר לקומפוננטה שמכילה אותה, שהיא באמצע לבצע פעולה וחבל על המאמץ של הרינדור בשלב הנוכחי.
איך היא מספרת את זה?

ראשית, הפונקציה שמבצעת את השליפה של המידע, צריכה להיות promise. אפשר להשתמש בfetch, axios methods, ועוד.
בדוגמא הבאה אני מציגה promise פיקטיבי לצורכי הבנה בלבד, ובו אני משתמשת לאורך הדוגמא:
קוד:
const mockFetchWithPromise = (): Promise<any> => {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id: 1, name: 'Rachel' }), 2000);
  });
}
הpromise הפיקטיבי שלי יחזיר את האובייקט { id: 1, name: ‘Rachel’ } בעיכוב של שתי שניות.

לאחר שהשליפה עטופה בpromise, מה שנשאר זה פשוט לזרוק אותו מתוך הקומפוננטה שמחכה למידע שהוא מחזיר. הpromise ייזרק לקלומפוננטה המכילה והיא תדע שהקומפוננטה User עדיין בהמתנה.
זריקה מתבצעת כך:
קוד:
throw mockFetchWithPromise();

בשלב זה הקומפוננטה User נראית כך:
קוד:
const mockFetchWithPromise = (): Promise<any> => {
    return new Promise((resolve) => {
      setTimeout(() => resolve({ id: 1, name: 'Rachel' }), 2000);
    });
  }
 
const SimpleUser: React.FC = () => {

    const [user, setUser] = useState<any | null>(null);

    throw mockFetchWithPromise();

    return <>
        {user && <><span>{user.id}</span>
            <span>{user.name}</span>
        </>}
    </>;
};

כעת כשנריץ את האפליקציה, לא יוצג התוכן של User אלא המילה Loading. מכיון שבכל פעם שריאקט מנסה לרנדר את User, נזרק אליו promise שמספר לו שהקומפוננטה ממתינה.

אבל זה כמובן עדיין לא מספיק. המטרה היא שכשהשליפה תסתיים, הקומפוננטה כן תתרנדר. בצורה הנוכחית היא תמיד זורקת promise, כך שהרינדור לעולם לא יתבצע.
בשביל לעשות את זה נכון, נעטוף את הpromise בפונקציה נוספת שמפעילה אותו, ומחזירה פונקציה שמאפשרת לקבל מידע על מצב השליפה:
קוד:
const initializeUserReader = () => {
 
  let data: any;
 
  let status: 'loading' | 'idle' | 'error' = 'loading';
  let error: any;

  // this function wrapping the promise and calls it immediatlly
  const fetchingUser = mockFetchWithPromise()
    .then((user) => {
      data = user;
      status = 'idle';
    })
    .catch((e) => {
      error = e;
      status = 'error';
    });

    // this function calls the promise, throws it while it's in progress, throws error if it's ended with error, and returns the data if it ended successfully
  const read = () => {
    if (status === 'loading') {
      throw fetchingUser;
    } else if (status === 'error') {
      throw error;
    }

    return data;
  }

  return read;
};
נעמיק מעט בקוד:
יש לנו פונקציה בשם initializeUserReader שהיא זאת ששולפת את פרטי הuser.
בתוך הפונקציה יש לנו שני משתנים:
fetchingUser - משתנה שעוטף את הpromise שמבצע את השליפה מהserver.
read - פונקציה פנימית שמאפשרת לקרוא את מצב הpromise. הפונקציה תזרוק את הpromise במקרה שהוא באמצע פעולה, תזרוק שגיאה אם הפעולה הסתיימה בשגיאה, ותחזיר את המידע במקרה שהפעולה הסתיימה בהצלחה.

כשאנחנו מפעילים את הפונקציה initializeUserReader, הpromise מתחיל לבצע את תפקידו, ובנוסף מוחזרת הפונקציה read לקריאת מצבו.

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

במקרה שלנו המטרה היא להתחיל לטעון את פרטי הuser מייד כשהאפליקציה עולה, ולכן נקרא לפונקציה הנ"ל בקומפוננטה App:
קוד:
const userReader = initializeUserReader();
ונשלח reference לקומפוננטה שצורכת את המידע:
קוד:
<Suspense fallback={<div>Loading...</div>}>
  <User userReader={userReader}  />
</Suspense>
הקומפוננטה User נראית כך:
קוד:
interface UserProps {
    userReader: { read: () => any };
}

const User: React.FC<UserProps> = (props: UserProps) => {

    const { userReader } = props;

    let user: any = userReader.read();

    return <div>
        <span>{user.id}</span>
        <span>{user.name}</span>
    </div>;
};

export default User;
זהו. עכשיו כשנריץ את האפליקציה, במשך שתי השניות הראשונות (עד שהpromise המאולתר יחזיר את פרטי הuser), יוצג לנו הטקסט Loading..., ולאחר מכן יוצגו פרטי המשתמש.


נ. ב.
לא התייחסתי למקרה שבו הpromise החזיר שגיאה.
הטיפול בכזה מקרה הוא לעטוף את Suspense בקומפוננטה נוספת ErrorBoundary. הקומפוננטה לא מובנית בשפה אבל אפשר לכתוב אותה לבד או להשתמש בספריות צד שלישי שמספקות אותה (כמו antd).
הסבר על ErrorBoundary אפשר למצוא כאן באנגלית ובעברית.

הסבר מעולה על promises למי שלא מכיר מספיק לעומק אפשר למצוא כאן בעברית.

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

6.gif