מסך ניהול יכול להיראות יציב במשך שבועות ואז להיפתח כריק בגלל שדה אחד ברשומה אחת. לא בגלל שהשרת כולו נפל, אלא מפני שקוד התצוגה הניח שכל ערך שמגיע אליו תקין.
בכלי לניהול קבצים שעבדתי עליו, התיקון האחרון עסק בדיוק במשפחה הזאת של כשלים: תאריך שלא ניתן לפרש, תצוגות מקדימות שנוצרו מחדש בזמן עריכה, ומסך שלא נתן למשתמש דרך ברורה להתאושש משגיאת רינדור. הלקח רחב יותר מגלריה: נתון פגום אחד לא צריך להפוך פעולה שלמה לבלתי אפשרית.
הבעיה: הנחה קטנה בתוך רשימה גדולה
גלריה מציגה פריטים רבים, ולכל אחד מהם כותרת, תמונה, גודל ותאריך עדכון. קל לכתוב פונקציה שמניחה שהתאריך הוא מחרוזת תקינה:
new Intl.DateTimeFormat('he-IL', { dateStyle: 'medium' })
.format(new Date(value));אבל `new Date(value)` יכול להחזיר אובייקט תאריך לא תקין בלי לזרוק שגיאה. רק הקריאה ל־`format` תזרוק `RangeError`. אם הפונקציה נקראת בזמן רינדור של פריט ברשימה, הכשל יכול לעלות עד לשורש המסך. משתמש שרצה לראות עשרות פריטים תקינים מקבל מסך ריק בגלל ערך אחד.
זהו פער בין חוזה הנתונים לבין חוזה התצוגה. גם אם השרת בדרך כלל שולח תאריך תקין, ממשק צריך להגדיר מה קורה כאשר שדה אופציונלי חסר או פגום.
התיקון הראשון: להגן על השדה במקום להפיל את הרשומה
במקרה הזה, תאריך העדכון אינו התנאי לצפייה בקובץ או לעריכת התיאור שלו. לכן אפשר לבדוק את הערך לפני הפורמט:
const formatDate = (value: string): string => {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
return new Intl.DateTimeFormat('he-IL', { dateStyle: 'medium' }).format(date);
};כאשר התאריך לא תקין, רק התאריך אינו מוצג. הכותרת, התמונה והפעולות נשארות זמינות. זו אינה הרשאה להתעלם מכל שגיאת נתונים: אם שדה קריטי לזהות הרשומה חסר, צריך לטפל בו אחרת. כאן בחרתי ב־degradation מקומי כי התאריך הוא מידע משני.
בדיקת רגרסיה שימושית אינה מסתפקת ב־`formatDate('')`. היא מרנדרת פריט עם תאריך חסר ומוודאת שגם הכותרת וגם פעולות העריכה עדיין מופיעות. כך הבדיקה מגנה על התנהגות המשתמש, לא רק על פונקציית העזר.
התיקון השני: תצוגה מקדימה היא משאב, לא ביטוי JSX
באותו מסך הייתה גם תצוגה מקדימה לקובץ שהמשתמש בחר להעלות. יצירת הכתובת ישירות בתוך הרינדור נראית תמימה:
<img src={URL.createObjectURL(file)} alt="" />אבל כל הקלדה בשדה התיאור יכולה לגרום לרינדור נוסף וליצירת כתובת `blob:` חדשה. הכתובות הישנות אינן משתחררות מעצמן ברגע שה־JSX מתחלף. תצוגה מקדימה היא משאב עם מחזור חיים: ליצור אותה כאשר הקובץ משתנה, ולהפעיל `URL.revokeObjectURL` כשהיא כבר אינה בשימוש.
העברתי את האחריות לרכיב preview קטן שמחזיק כתובת אחת לכל אובייקט `File`, משחרר אותה בניקוי, ומציג חלופה אם יצירת הכתובת נכשלת. בדיקה שמדמה עריכה חוזרת של התיאור מוודאת ש־`createObjectURL` נקראת פעם אחת, ולא פעם לכל הקשה.
זה תיקון נפרד משגיאת התאריך. הוא לא מסביר לבדו מסך לבן, אבל הוא מסיר דליפת משאבים ואת התלות הלא־נכונה בין עריכת טקסט לבין יצירת preview.
רשת ביטחון למסך, בלי להסתיר את הבעיה
גם אחרי הגנה על השדות הידועים, רכיב יכול לזרוק שגיאה לא צפויה. `Error Boundary` ברמת המסך מאפשר להציג הודעת התאוששות וכפתור רענון במקום להשאיר חלל ריק. במקביל, השגיאה נרשמת כדי שאפשר יהיה לחקור את המקור שלה.
חשוב לשמור על ההבחנה: ה־boundary אינו מתקן את הנתון הפגום. הוא רק מצמצם את הנזק כאשר תיקון נקודתי לא הספיק. וגם הודעת ההתאוששות חייבת להיות מדויקת: אין לומר שקובץ נשמר בהצלחה אם הפעולה לא אומתה.
מה למדתי
אמינות של ממשק לא נמדדת רק בשאלה אם ה־API עונה. היא נמדדת גם בשאלה מה קורה כששדה אחד אינו עומד בציפיות:
- שדה משני פגום צריך לפגוע בשדה הזה, לא בכל המסך.
- משאב שנוצר בדפדפן צריך בעלות ומחזור חיים ברורים.
- מסך התאוששות הוא רשת ביטחון, לא תחליף לתיקון שורש הבעיה.
- הבדיקה החשובה היא מה המשתמש עדיין יכול לעשות אחרי שהנתון השתבש.
העיקרון שלקחתי מהתיקון הזה הוא פשוט: ככל שהכשל מקומי יותר, כך גם ההשפעה שלו על המשתמש צריכה להיות מקומית יותר.