• בלוג
  • ארבע תבניות לקומפוננטות React מתורגמות ל ClojureScript ו Reagent

ארבע תבניות לקומפוננטות React מתורגמות ל ClojureScript ו Reagent

11/04/2020

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

 

1. קומפוננטת מונה לחיצות

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

בגירסת ריאקט מונה הלחיצות נראה כך:

function StandaloneCounter(props) {
const [clicks, setClicks] = React.useState(0);
const clicked = () => setClicks(v => v + 1);

return (
<div>
<p>Clicks: {clicks}</p>
<button onClick={clicked}>Click Me</button>
</div>
)
}

והתרגום שלו ל Reagent:

(defn standalone-counter []
(r/with-let [clicks (r/atom 0)
clicked (fn [] (swap! clicks inc))]

[:div
[:p "Clicks: " (str @clicks)]
[:button {:onClick clicked} "Click Me"]]))

אז בספירת שורות קלוז'ר מנצח בגלל שתחביר ה HTML (נקרא hiccup) ויתר על התג הסוגר. מצד שני בחיים האמיתיים גם ככה התרגום מ HTML רגיל ל HTML של ריאקט יכול להעיק (כי הרבה פעמים אנחנו מקבלים את ה HTML ממישהו אחר), אז המעבר ל Hiccup עלול רק להחמיר את הבעיה.

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

2. מונה לחיצות עם סטייט משותף

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

קלוז'רסקריפט זורק את המגבלה הזאת של ריאקט על הסטייטים ומאפשר לנו ממש בקלות לשתף סטייט בין קומפוננטות. הקוד נראה כך:

(defn counter-item [{:keys [clicks]}]
  (let [clicked (fn [] (swap! clicks inc))]
    [:div
     [:p "Clicks: " (str @clicks)]
     [:button {:onClick clicked} "Click Me"]]))

(defn counter-group [{:keys [qty]}]
  (r/with-let [counters (for [_ (range qty)] (r/atom 0))]
    [:<>
     [:p "Max = " (str @(apply max-key deref counters))]
     (for [clicks counters] [counter-item {:clicks clicks}])]))

הסטייט של כל המונים נשמר בקומפוננטה counter-group, אבל כל מונה יכול להעלות את הערך של ה Counter שמשויך אליו. זה קצת כמו להעביר גם את הסטייט וגם פונקציית עדכון מהאבא לילדים בריאקט, אבל חוסך לי להגדיר את פונקציית העדכון.

3. טיימר

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

(defn timer []
  (r/with-let [
               ticks (r/atom 0)
               clock (js/setInterval #(swap! ticks inc) 1000)
               ]
    [:span "Ticks ... " (str @ticks)]
    (finally (js/clearInterval clock))))

אנחנו מגדירים את ticks כמשתנה סטייט, ומפעילים את setInterval כדי לעדכן אותו כל שניה. בבלוק ה finally של הקומפוננטה אנחנו מנקים את השעון כדי שאם הקומפוננטה תצא מהמסך השעון יעצור.

מבחינת תחביר גם כאן מאוד מועיל להשוות עם React/JavaScript. הפונקציה המקבילה נראית כך:

function Timer(props) {
  const [ticks, setTicks] = React.useState(0);

  React.useEffect(function() {
    const clock = setInterval(function() {
      setTicks(t => t + 1);
    }, 1000);
    return function() {
      clearInterval(clock);
    }
  }, []);

  return (<p>Ticks: ... {ticks}</p>)
}

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

4. תקשורת

תבנית אחרונה היא תקשורת Ajax. כאן ריאייג'נט איבד אותי (או שאני איבדתי אותו) כיוון שאחרי עבודה עם useEffect בריאקט היו לי ציפיות מסוימות לאיך תקשורת Ajax צריכה לעבוד. נתחיל עם גירסת הריאקט הפעם:

import React from "react";
import $ from 'jquery';
import { useState, useEffect } from 'react';

export function ShowCharacterInfo(props) {
  const { data } = props;
  return (
    <>
    <p><b>Name:</b> {data.name}</p>
    <p><b>base_experience:</b> {data.base_experience}</p>
    </>
  )
}

export function Pokemon(props) {
  const [data, setData] = useState(null);
  const { id } = props;

  useEffect(function() {
    setData(null);
    const $xhr = $.getJSON(`https://pokeapi.co/api/v2/pokemon/${id}/`, setData);

    return function abort() {
      $xhr.abort();
    }
  }, [id]);

  return (
    <div>
      <pre>Debug: id = {id}</pre>
      {data ? <ShowCharacterInfo data={data} /> : 'Loading, please wait'}
    </div>
  );
}

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

ריאייג'נט לא כוללת תמיכה ב useEffect. הקוד הבא הוא ניסיון שלי שלא עבד לבנות משהו דומה ב Reagent:

(defn pokemon-character [{id :id}]
  (r/with-let [
               data (r/atom {})
               active (r/atom true)
               request (go
                         (let [response (<! (http/get 
                                              (str "https://pokeapi.co/api/v2/pokemon/" @id)
                                              {:with-credentials? false}))]
                           (if (true? @active) (reset! data (:body response)) nil)))
               ]
    [:div
     [:p "ID: " @id]
     [:p "Name: " (str (get @data :name))]]))

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

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