צרות של ריאקטיביים

13/06/2023

בעבודה עם ריאקט אנחנו אוהבים להגיד שסטייט שייך לקומפוננטה, במובן זה שכשסטייט משתנה ריאקט צריך לחשב מחדש איך נראית הקומפוננטה. בגירסת הקלוז'ר של ריאקט שנקראת Reagent, הכניסו מנגנון נוסף וריאקטיבי לניהול סטייט - לא רק הסטייט הפנימי המשויך לקומפוננטה של useState, אלא יש להם גם סטייט מיוחד שנקרא r/atom שיכול להיות מוגדר בכל מקום בתוכנית, ושינוי שלו גם יגרום לחישוב מחדש של הקומפוננטה.

עכשיו הבעיה שלהם הפכה להיות איך לזהות ש r/atom כזה השתנה, והתשובה היא ריקטיביות. בדומה ל Vue ועוד יותר ממנו ל Solid, גם ריאייג'נט מסתכל בזמן חישוב הקומפוננטה לאיזה אטומים ניגשו, ומחבר אותם לקומפוננטה כך ששינוי בערך האטום יגרום לחישוב מחדש של הקומפוננטה.

לכן הקוד הבא ל Counter עובד:

(defn counter []
  ;; run on init, skip on re-calculations
  (r/with-let [value (r/atom 0)]
    [:div
     {:style {:margin "10px"}}
     [:p (str "Value " @value)]
     [:button {:on-click #(swap! value inc)} "+"]
     [:button {:on-click #(swap! value dec)} "-"]]))

ומציע מנגנון קלוז'רי הדומה מספיק לריאקט בשביל לא להפחיד.

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

(defn textboxes []
  (r/with-let [value (r/atom "hello")]
    [:div
     (for [i (range 5)]
       [:input {:value @value
                :on-change (fn [ev] (reset! value (.. ev -target -value)))}])]))

הקוד אמור להציג 5 תיבות טקסט כך ששינוי טקסט באחת התיבות יועתק לכל האחרות. זה לא קורה.

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

התיקון הוא פשוט אחרי שמבינים את הבעיה - רק צריך להפוך את ה for מלולאה עצלה ללולאה מלאה עם doall והכל מסתדר:

(defn textboxes []
  (r/with-let [value (r/atom "hello")]
    [:div
     (doall
      (for [i (range 5)]
        [:input {:value @value
                 :on-change (fn [ev] (reset! value (.. ev -target -value)))}]))]))