רוב קוד ה-frontend מתייחס לעבודה אסינכרונית כאל צורה אחת אחידה. אתם מפעילים Promise, מחכים שהוא יסתיים (resolve), שופכים את התוצאה ל-state מקומי, ומאפשרים ל-framework לבצע reconciliation להבדלים. התבנית הזו מפתה כי היא עובדת בכל מקום: קריאת REST, שליחת טופס, הודעת WebSocket. כולן נוחתות באותו useEffect או בתוך event handler, כולן עוברות דרך setState, וכולן נראות כמו תשתית אסינכרונית זהה. האחידות הזו היא מלכודת. באפליקציה אמיתית, לא כל העבודה האסינכרונית היא אותו דבר. ההתחזות לכך שהיא כזו הופכת את רכיבי ה-UI שלכם לאדריכלי נתונים מקריים, שנתפרים יחד באמצעות hooks של useEffect ותקוות לאלוהים.

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

Queries: עובדות עם כתובת

שאילתה (Query) היא לא רק fetch. זוהי בקשה לעובדה ניתנת לזיהוי. אתם מבקשים את /user/123, לא "נתוני משתמש כלשהם". ההבחנה הזו חשובה כי זהות היא מה שמאפשר caching. אם שני רכיבים באותו מסך זקוקים לאותה רשומת משתמש, הם צריכים לחלוק תשובה אחת. כשכל רכיב שומר עותק מקומי משלו ב-useState, אתם מפצלים את האמת שלכם. ה-avatar ב-header והשם ב-sidebar מתרחקים זה מזה כי הם נשלפו בזמנים שונים, או שאחד נכשל בעוד השני הצליח.

חשבו על Query כעל משאב (resource), לא כעל פעולה (action). יש לה cache key, מדיניות רעננות (freshness policy) ומחזור חיים שחי מעבר לכל רכיב בודד. שכבת Query בנויה היטב מבינה שקריאה ל-/projects?page=2 שונה מקריאה ל-/projects?page=3. כל URL וסט של פרמטרים מהווה כתובת, ולנתונים בכתובת הזו יכול להיות ערך ישן (stale), טרי או חסר. ה-UI לא אמור להיות אחראי על הניהול הזה. הוא צריך לבקש משכבת הנתונים את user:123 ולקבל snapshot. העובדה שה-snapshot הזה הגיע מהשרת לפני שתי שניות או מה-cache לפני שני מילישניות היא לא עניינו של הרכיב.

ההשלכה המעשית היא מיידית. כשמתייחסים לכל קריאה כאל fetch אימפרטיבי בתוך רכיב, מאבדים את ה-deduplication. מאבדים את ה-background refresh. מאבדים את היכולת להציג נתוני cache באופן מיידי תוך כדי אימות שלהם ברקע. ל-Query מגיע בית מחוץ לעץ ה-UI שלכם.

Mutations: לשנות את העולם

אם Queries שואלים על העולם, Mutations משנות אותו. בקשת הרשת בפועל — ה-POST, PUT, או DELETE — היא בדרך כלל החלק הקל. החלק הקשה הוא כל מה שקורה אחרי שהשרת אומר "OK".

נניח שמשתמש מעדכן את שם התצוגה שלו. ה-mutation עצמו הוא בקשה בודדת. אבל רדיוס ההשפעה הוא בכל מקום. דף הפרופיל מחזיק את השם הישן. סרגל הניווט מציג את השם הישן. היסטוריית התגובות עשויה להתייחס אליו. אם קוד ה-mutation שלכם לא עושה דבר מלבד החלפת דגל isLoading מקומי ואז עדכון חלק אחד ב-state, האפליקציה שלכם משקרת לעצמה. חלקים ב-UI מעמידים פנים שהשינוי קרה. אחרים לא יודעים בכלל שמשהו השתנה.

Mutation חייב להצהיר על ההשפעה שלו על גרף הנתונים (data graph). הוא צריך להגיד למערכת אילו Queries כעת אינם תקפים, אילו מפתחות ב-cache צריכים להישלף מחדש, ואילו קשרים השתנו. זה שונה מהותית מ-Query. Query הוא לקריאה בלבד וניתן לשיתוף. Mutation מתמקד בכתיבה והוא הרסני עבור caches קיימים. איחוד שלהם לאבסטרציה אחת משמעותו שמתכנתים יסיימו לקרוא ל-refetch() באופן ידני בתוך רכיבים אקראיים, או גרוע מכך, יפזרו hooks של useEffect ברחבי העץ כדי "לסנכרן" את ה-state המקומי בחזרה עם השרת.

גם מודל הבעלות שונה. Queries בדרך כלל נמצאים בבעלות ה-cache. Mutation נמצא בבעלות פעולת המשתמש שהפעילה אותו. יש לו מצב המתנה (pending state), מצב שגיאה (error state), ופוטנציאל לערך אופטימיסטי (optimistic value) שצריך להתגלגל