מדוע מצב האפליקציה ב-MobX React נשבר עם צמיחת הממשק
כאשר הממשק גדל מעשרה מסכים למאה, המצב מפסיק להיות פרט מימוש והופך למקור לתקלות. בפרויקט ב-mobx react התרחיש הטיפוסי הוא כזה: אותו אובייקט נערך משלושה מקומות, צופים רשומים לפרוסות רחבות מדי, וטעינת נתונים מתרחשת במקביל לפעולות המשתמש.
ניהול מצב react נשבר לא ברגע כתיבת הקוד, אלא בקנה מידה — כאשר מופיעות טפסים מקוננים, מסננים עם שמירה, טיוטות ורענון ברקע.
המחלקה הראשונה של בעיות — חוסר סנכרון. הנתונים משוכפלים בכמה מאגרים, ולאחר השמירה אחד מהם נשאר עם העתק ישן: המשתמש רואה דבר אחד ברשימה ואחר בכרטיס.
השנייה — רינדורים מיותרים של קומפוננטות: צופה רשום לאובייקט שלם כולו, למרות שהוא קורא שדה אחד, וכל שינוי בעץ גורר אחריו רינדור של כל תת-העץ. בטבלאות גדולות זה מורגש כ"תקיעות" של הממשק בעת הקלדה.
המחלקה השלישית — מרוצים בזמן טעינה. המשתמש מחליף מסננים במהירות, התשובות מגיעות שלא בסדר הנכון, ועל המסך מופיעים נתונים של השאילתה הקודמת. אותו אפקט נותנת ביטול שאילתה בלי איפוס מצב: הטעינה הסתיימה, אך הדגל "טעינה מתבצעת" נשאר דלוק.
הרביעית — תלויות סמויות בין מאגרים: ערך מחושב אחד קורא לאחר, וכאשר סדר האתחול משתנה גם ההתנהגות משתנה. תקלות כאלה אינן משוחזרות על סט הבדיקות וצפות רק תחת עומס.
אנחנו מפרקים כיצד זה נסגר: היכן גבולות המאגרים, אילו פרוסות למסור לצופים וכיצד לבנות את סדר הטעינה כך שהמצב יישאר צפוי.
מה נותן ניהול מצב בנוי היטב ב-MobX React
חנויות נצפות במקום סנכרון ידני
ב-MobX React המצב חי בחנויות נצפות: הרכיב נרשם רק לשדות שהוא באמת קורא בזמן הרינדור. העדכונים הריאקטיביים ממוקדים — כאשר ישות אחת משתנה, לא נבנה מחדש עץ שלם, אלא צומת ספציפי.
במסכים צפופים (טבלאות, מסננים, טפסים עם עשרות שדות) זה מסיר את הסיבה העיקרית ל"איטיות בהקלדה": מעברי רינדור מיותרים просто לא מופעלים.
ארכיטקטורת מצב כחוזה
אנחנו פורסים את המצב לשכבות: חנויות דומיין, חנויות מסך וערכים מחושבים נגזרים. הנגזרים מחושבים בעצלות ונשמרים במטמון — חישוב מחדש מתבצע רק כאשר התלויות משתנות, ולא בכל רינדור. זה נותן צפיות לעדכונים: הסיבה לשינוי המצב ומקום הרינדור קשורים באופן מפורש, ולא דרך שרשרת אפקטים.
מה יוצא ממחלקת הכשלים
אפקט נפרד — נעלמות מרוצים בבקשות אסינכרוניות: תגובה מיושנת לא דורסת נתונים עדכניים, כי הכתיבה לחנות מתבצעת דרך פעולה מפורשת. דליפות זיכרון מהרשמות שנשכחו גם נעלמות: ההרשמה חיה בדיוק כל עוד הרכיב חי.
| אפשרות | מתי מתאימה | מגבלות |
|---|---|---|
| MobX | ישויות רבות קשורות, עדכונים ממוקדים תכופים, טפסים מורכבים | נדרשת משמעת: בלי פעולות מפורשות המצב מתפזר |
| מצב מקומי של רכיב | כמה שדות במסך אחד, אין נתונים משותפים | לא מתרחב: המצב משוכפל בין מסכים |
| חנות גלובלית עם הרשמה ידנית | אפליקציות פשוטות עם עדכונים נדירים | סנכרון ידני יוצר רינדורים מיותרים ומרוצים |
MobX נבחר לא כאופנה, אלא ככלי לפרופיל עומס ספציפי: שינויים ממוקדים תכופים עם גרף נתונים קשור גדול. לפני ההטמעה אנחנו מצלמים פרופיל רינדור על הקוד הנוכחי — לפיו רואים כמה זמן הולך למעברים מיותרים ואילו מסכים מתדרדרים ראשונים.
כיצד אנו מגדירים מצב: שלבי עבודה הנדסית
הטמעת MobX באפליקציית React אינה מתחילה בהתקנת החבילה, אלא בביקורת של האופן שבו המצב חי כיום: היכן נשמרים הנתונים, מי ומתי משנה אותם, כמה קומפוננטות מתרנדרות מחדש בכל עדכון.
בשלב זה אנו מזהים את הנקודות הבעייתיות — כפילות נתונים בין החנויות, תלויות סמויות, עדכונים שגוררים רינדורים מיותרים. לאחר מכן אנו בונים את הסכימה כך שכל שינוי במצב יהיה צפוי ולוקאלי.
- ביקורת המצב. מפרקים את הארכיטקטורה הנוכחית: מפת החנויות, זרימת הנתונים, נקודות המוטציה, צווארי בקבוק במספר הרינדורים. התוצאה — סכימת מצב עם גבולות ברורים בין הדומיינים.
- תכנון החנויות. מחלקים את המצב לחנויות בלתי תלויות לפי תחומים עסקיים, מגדירים שדות נצפים, ערכים מחושבים ופעולות. זה מסיר את הצימוד: שינוי בדומיין אחד אינו גורר רינדור מחדש של כל העץ.
- הטמעת MobX. מחברים את ספק החנויות, מגדירים מצב קפדני כך שמוטציות יעברו רק דרך פעולות. הקומפוננטות נרשמות באופן ממוקד — מתעדכן רק מה שתלוי בפועל בשדה שהשתנה.
- בדיקת המצב. מכסים בבדיקות ערכים מחושבים, פעולות ומעברים בין מצבים, כולל חזרה לאחור בעת שגיאה. הבדיקות מתעדות את ההתנהגות לפני הריפקטורינג ואחריו — רגרסיות נראות מיד.
- אופטימיזציה של עדכונים. מודדים את מספר הרינדורים לפני ואחרי, מסירים רישומים מיותרים, מוציאים חישובים כבדים לערכים הניתנים לשמירה במטמון.
- מסירת תיעוד. מכינים סכימת חנויות, כללים להוספת שדות ופעולות חדשים, תיאור הקונבנציות. צוות הלקוח מקבל לא רק קוד, אלא גם תקנון ברור כיצד לעבוד איתו בהמשך.
- הדרכת הצוות. עורכים ניתוח על משימות אמיתיות של הפרויקט: כיצד להוסיף חנות, כיצד לקשר אותה לקומפוננטה, כיצד לא לשבור רישומים קיימים.
סדר זה מניב תוצאה מדידה: מספר הרינדורים המיותרים יורד, והתנהגות המצב הופכת לשחזורית בבדיקות. אם יש לכם כבר אפליקציית React עם מצב מסובך — תארו את המשימה, נחזור עם הערכה ותוכנית עבודה.
במה נבדלות הגישות לניהול מצב ב-React?
ב-React ישנן ארבע דרכים מעשיות להחזיק מצב: useState מקומי, Context API, Redux ו-MobX. מצב מקומי סוגר טופס, מודאל או מסנן — כל עוד הנתונים אינם נדרשים מחוץ לתת-העץ.
ברגע שאותו ערך נקרא בחמישה מסכים ובשלושה תהליכי רקע, מתחיל בחירת ספריית המצב, וכאן mobx react מנצחת במקומות שבהם מהירות הפיתוח חשובה, ו-redux vs mobx מוכרע לטובתה בעדכונים נקודתיים תכופים.
Context API פותר את העברת הנתונים למטה, אך לא את ניהולם: כל שינוי בערך מרנדר מחדש את כל צרכני הספק. ברשימה של מאות שורות זה הופך לירידה בתגובתיות, ו-Context אינו יודע להעביר את לוגיקת העדכון לרדוסרים — יש להוסיף זאת ידנית.
Redux מספק צפיות דרך סטור יחיד, רדוסרים טהורים וזרימת פעולות קפדנית, אך המחיר הוא רב-מילות: פעולה אחת לכל שינוי, סלקטורים נפרדים, נרמול ישויות. בצוותים גדולים זה יתרון, במוצר קטן — האטה.
MobX בונה מצב על ערכים נצפים ושדות מחושבים. הרכיב נרשם רק לנתונים שהוא באמת קורא, ומרנדר מחדש באופן נקודתי — ללא השוואה ידנית וללא ממואיזציה.
ערכים מחושבים נשמרים במטמון ומחושבים מחדש רק כאשר התלויות משתנות: נתונים נגזרים כמו סיכומי סל או רשימה מסוננת אינם מחושבים מחדש בכל רינדור. המוטציות עצמן ישירות — השמה לסטור במקום דיספאץ' של פעולה, מה שמצמצם את נפח הקוד פי כמה.
גם למגבלות הגישה יש אופי קונקרטי. מוטציות ישירות דורשות משמעת: ללא מצב קפדני מופעל קל לשנות מצב מחוץ לפעולה, והדיבוג הופך מורכב יותר. תלויות מרומזות בין שדות נצפים יוצרות "קסם" — למהנדס חדש בצוות נדרש זמן כדי לראות מי ומתי משנה ערך.
בתרחישים עם ביקורת קפדנית של כל שינוי (פעולות פיננסיות, יומן אירועים) זרימת הפעולות המפורשת של Redux נקראת שקופה יותר.
כלל מעשי: Context — לנושא, לשפה, להרשאה, כלומר לנתונים שמשתנים לעתים רחוקות. Redux — כאשר נדרשת ציר זמן של שינויים, שחזור מצבים וחוזים קפדניים בין צוותים. MobX — לממשקים עם זרימת עדכונים צפופה: טבלאות, עורכים, דשבורדים, סלים, שבהם חשובים רינדורים נקודתיים ומינימום עטיפה.
בחירת ספריית המצב אינה דת, אלא שאלה של כמה פעמים בשנייה משתנים הנתונים וכמה בקרה קפדנית על השינויים נדרשת למוצר.
מקרה בוחן: כיצד חיסלנו אי-סנכרון נתונים באזור האישי
אזור אישי עם כמה בלוקים בלתי תלויים — פרופיל, התראות, היסטוריית פעולות — הופך במהירות לאוסף של מצבים כפולים. כל בלוק משך את הנתונים שלו והחזיק עותק משלו, והסנכרון ביניהם התבצע דרך שרשרת של עדכונים ידניים.
בפועל זה יצר אי-סנכרון קלאסי: המשתמש שינה נתונים באזור אחד, ובאחר עדיין היה תלוי מענה מיושן. עסקנו בכך בפרויקט שבו הממשק מבוסס mobx react הלך והסתעף בקשרים כאלה ככל שגדל.
מה היה לפני השינויים
מדידות לפני העבודה מחדש הראו שלוש בעיות מערכתיות. תגובת הממשק לפעולת המשתמש הגיעה עד 1.8 שניות — הרינדור מחדש נגע בכל העץ, אף שהשתנה בלוק אחד בלבד. שגיאות מצב נרשמו בממוצע 12 פעמים לאלף סשנים: במקום אחד דגל הטעינה לא אופס, במקום אחר נותרה רשימה מיושנת. טעינת האזור האישי ארכה כמעט 4 שניות, מכיוון שאותם נתונים בדיוק נדרשו משלושה מקומות במקביל.
מה שינינו
צמצמנו את המצב למאגר единый עם אזורי אחריות ברורים: מודולים נפרדים לפרופיל, להתראות ולהיסטוריה, ואת הנתונים המשותפים הוצאנו למקור אחד. את הרכיבים העברנו להרשמה ממוקדת — כעת התרנדר מחדש רק הבלוק שהנתונים שלו השתנו, ולא כל המסך.
פעולות אסינכרוניות קיבלו מחזור חיים צפוי: בקשה, מצב טעינה ותוצאה חיים במקום אחד, וקריאות כפולות התאחדו לאחת.
מה קרה אחרי
תגובת הממשק הצטמצמה ל-200 מילישניות — כמעט פי תשעה. שגיאות המצב ירדו מ-12 ל-1-2 לאלף סשנים. טעינת האזור האישי הואצה ל-1.2 שניות, מכיוון שנעלמו בקשות חוזרות לאותם נתונים.
בנפרד נציין את הצפיות: לאחר העבודה מחדש, בלוקים חדשים מתווספים ללא שינויים במודולים של אחרים, וההתנהגות בעת כשל רשת הפכה אחידה בכל האזורים.
MobX
ספקות לגבי תאימות, מיגרציה וחזרה לאחור — מפרקים
תאימות MobX עם הסטאק הנוכחי כמעט אף פעם אינה בעיה: הספרייה פועלת מעל React ואינה דורשת לכתוב מחדש את הקומפוננטות כולן. מאגרים נצפים (observable stores) מחוברים באופן ממוקד — תחילה מודול אחד, אחר כך השאר — ולכן האפליקציה נשארת עובדת בכל שלב.
המגבלה היחידה: אם בפרויקט כבר קיימת גישה שנייה לניהול מצב, חלק מהלוגיקה יצטרך להיות מרוכז למקור אמת אחד, אחרת ייווצרו אי-התאמות.
מה אם יידרש חזרה לאחור? השינויים מבוצעים במנות נפרדות, כל אחת — סט עצמאי של עריכות עם נקודת חזרה. כל עוד הסכמה החדשה לא אומתה בתרחישים אמיתיים, הישנה נשארת בקוד ומופעלת באמצעות מפסק. חזרה לאחור היא החזרת הגדרה אחת, ולא שחזור מגיבוי.
מה לגבי הרשאות ותפקידים? מצב הסשן, ההרשאות והתפקידים מוצא למאגר נפרד עם כללים מפורשים: מי יוזם שינוי, מה רואה המשתמש, כיצד מתנהג הממשק עם פקיעת הסשן. זה מסיר סוג שלם של שגיאות שבהן לאחר רענון הדף ההרשאות "נופלות".
עד כמה קשה להעביר את הפרויקט לצוות אחר?
המצב מתואר באופן דקלרטיבי: המודל, הפעולות, התגובות לשינויים נמצאים במקומות צפויים, ולא מפוזרים בין הקומפוננטות. מהנדס חדש קורא את המודל ומבין את זרימת הנתונים בלי לנתח את היסטוריית העריכות. העברת תיעוד היא לא תיקיית קבצים, אלא מפה עובדת: היכן חי המצב, כיצד הוא משתנה, מה נבדק בבדיקות.
עם סיום העבודה הלקוח מקבל לידיו:
- סכמת מאגרי המצב עם תיאור הקשרים בין המודולים;
- כללי מיגרציה של המצב וסדר החזרה לאחור של שינויים;
- מטריצת הרשאות ותפקידים עם התנהגות הממשק בכל מקרה;
- סט בדיקות המכסות את תרחישי המפתח של שינוי נתונים;
- הוראות להרצה מקומית ולשחזור סביבת העבודה;
- יומן החלטות: מדוע נבחרה גישה זו ואילו חלופות נדחו.
תארו את המשימה ואת הסטאק הנוכחי — נחזור עם הערכה ותוכנית עבודה, כולל סדר המיגרציה ונקודות החזרה לאחור.







