הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

למה צריך ואיך לנהל State גלובאלי של יישום ריאקט עם MobX

30/07/2020

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

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

המשך קריאה

הבדיקות ממש הצילו אותי הפעם

29/07/2020

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

״הבדיקות ממש הצילו אותי הפעם״

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

ואם אתם לא שומעים אותו מספיק פעמים זה אומר אחד משני דברים: או שאתם לא עושים מספיק Refactoring, או שהבדיקות שלכם לא מספיק טובות.

דוגמת Refactoring ב React כדי לקבל Markup נקי יותר

28/07/2020

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

במילים אחרות אם כתבתי 2 בתיבת השעות אוטומטית יופיע 120 בתיבת הדקות ו 7200 בתיבת השניות. ואם כתבתי 60 בתיבת הדקות אוטומטית יופיע 1 בתיבת השעות ו 3600 בתיבת השניות. אני משנה טקסט בתיבה אחת והשתיים האחרות צריכות להתעדכן בהתאמה.

הרבה מאוד פעמים אנשים יפתרו את התרגיל עם קוד שנראה בערך כך:

function TimeConverter(props) {
    const [seconds, setSeconds] = useState(0);

    return (
        <div>
            <label>
                Seconds:
                <input
                    type="number"
                    value={seconds}
                    onChange={e => setSeconds(e.target.value)}
                />
            </label>
            <label>
                Minutes:
                <input
                    type="number"
                    value={seconds / 60}
                    onChange={e => setSeconds(e.target.value * 60)}
                />
            </label>
            <label>
                Hours:
                <input
                    type="number"
                    value={seconds / 3600}
                    onChange={e => setSeconds(e.target.value * 3600)}
                />
            </label>
        </div>
    );
}

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

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

function TimeConverter(props) {
    const time = useTime(0);

    return (
        <div>
            <SecondsPanel time={...time} />
            <MinutesPanel time={...time} />
            <HoursPanel time={...time} />
        </div>
    );
}

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

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

function useTime(initialValue) {
    const [value, setter] = useState(initialValue);
    return {
        time: value,
        setTime: setter,
    };
}

בשלב ראשון אני יכול לכתוב את שלושת הקומפוננטות עם אותו קוד שהיה לי ב JSX הראשוני:

function Seconds(props) {
    const { time, setTime } = props;

    return (
                <label>
                    Seconds:
                    <input
                        type="number"
                        value={time}
                        onChange={e => setTime(e.target.value)}
                    />
                </label>
    );
}

function Minutes(props) {
    const { time, setTime } = props;
    return (
            <label>
                Minutes:
                <input
                    type="number"
                    value={time / 60}
                    onChange={e => setTime(e.target.value * 60)}
                />
            </label>
    );
}

function Hours(props) {
    const { time, setTime } = props;
    return (
            <label>
                Hours:
                <input
                    type="number"
                    value={time / 3600}
                    onChange={e => setTime(e.target.value * 3600)}
                />
            </label>
    );
}

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

function timeUnit(name, factor) {
  const TimeUnit = (props) => {
    const { time, setTime } = props;
    return (
      <label>
        {name}
        <input
          type="number"
          value={time / factor}
          onChange={e => setTime(e.target.value * factor)}
          />
      </label>
    )
  }
  TimeUnit.name = name;
  return TimeUnit;
}

ובאמצעותה ליצור את הקומפוננטות:

const Seconds = timeUnit('Seconds', 1);
const Minutes = timeUnit('Minutes', 60);
const Hours = timeUnit('Hours', 3600);

התוצאה היא קוד נקי בהרבה שגם אפשר בקלות להרחיב אותו לממירים מסוגים אחרים (למשל ממיר מטבעות):

function timeUnit(name, factor) {
  const TimeUnit = (props) => {
    const { time, setTime } = props;
    return (
      <label>
        {name}
        <input
          type="number"
          value={time / factor}
          onChange={e => setTime(e.target.value * factor)}
          />
      </label>
    )
  }
  TimeUnit.name = name;
  return TimeUnit;
}

function useTime(initialValue=0) {
    const [value, setter] = React.useState(0);
    return { time: value, setTime: setter };
}

const Seconds = timeUnit('Seconds', 1);
const Minutes = timeUnit('Minutes', 60);
const Hours = timeUnit('Hours', 3600);

function TimeConverter(props) {
  const time = useTime();

  return (
    <div>
      <Seconds {...time} />
      <Minutes {...time} />
      <Hours {...time} />
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('main'));

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

כמה זה עולה לנו?

27/07/2020

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

האם וובסטורם שווה יותר מ Sublime Text כי הוא עולה 59$ בשנה וסאבליים 80$ לשלוש שנים? האם בייסקמפ שווה יותר או פחות מג'ירה? וזה עוד החלק הקל כי המחיר הוא בדולרים.

החלק הקשה באמת הוא להעריך כמה עולה לי להוסיף ספריית קוד או להשתמש בתוכנה חינמית. אף אחד לא יספר לכם כמה "עולה" להשתמש ב Redux לעומת MobX, או ב vim לעומת emacs. אין לנו מושג כמה זמן ייקח לך או לצוות שלך להבין את הכלי, האם תשתמשו בו נכון או האם הוא רק יסבך אתכם יותר. בשביל להבין אם כלי מסוים שווה לי אני צריך להיות מסוגל לענות כמה הוא בכלל עולה, ובספריות קוד פתוח התשובה מאוד מורכבת.

שתי דוגמאות מהחיים-

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

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

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

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

איזה כיף שיש כבר Generators ב JavaScript

26/07/2020

הרבה אומרים שאופטימיות מובילה להצלחה בחיים. בתכנות בכל מקרה האופטימיות מביאה בדרך כלל למבנה קוד שנקרא רקורסיה. יש לזה אפילו ביטוי באנגלית: Wishful Thinking. בואו נראה איך זה קורה ואיך זה קשור ל Generators.

המשך קריאה

ארגז כלים

25/07/2020

כשרואים ארגז כלים יש פיתוי גדול לחפש את הכלי הכי חזק בארגז וללמוד אותו. הנחת העבודה היא שממילא כל מה שאפשר לעשות עם הכלים הפשוטים יותר אפשר יהיה לעשות גם עם הכלי המתוחכם. אבל הפיתוי הזה מוביל לגישות לא נכונות בתכנות-

״אין טעם ללמוד וים, אני כבר יודע טוב להשתמש ב Webstorm שעובד טוב יותר״

״אין טעם להשקיע ב Redux, אני כבר יודע MobX שעושה הכל טוב יותר״

״אני ממילא עובד רק עם ממשק ה git שמשולב ב IDE שלי, אז אין טעם ללמוד לעבוד איתו משורת הפקודה״

בשביל ללמוד בשמחה כלים חדשים כדאי לנו לבחור סיפור אחר לספר לעצמנו. משהו כמו ״כל כלי חדש שאני לומד פותח לי את המחשבה לדרך חשיבה חדשה״, או בדוגמאות:

״אני כבר יודע Webstorm, אז בטח יהיה מעניין לראות איך עושים פעולה דומה ב vim״

״כתבתי כבר שני פרויקטים ב MobX, אז נראה לי שעכשיו אני מוכן להתמודד עם Redux לפרויקט הבא״

״החודש אני הולך להשתמש ב git משורת הפקודה בלבד - מעניין מה זה ילמד אותי על הכלי שעדיין לא ידעתי״

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

אבל זה רק פסיק קטן

24/07/2020

קבלו שאלת ראיונות עבודה זריזה על ריאקט שעלתה בקורס היום - מה ההבדל בין שתי הקומפוננטות האלה, ובאיזה נעדיף להשתמש:

// Component 1
function App() {
  const [value, setValue] = React.useState(_.random(1, 100));

  return (
    <div>
      <button onClick={() => setValue(c => c + 1)}>{value}</button>
    </div>
  );
}
// Component 2
function App() {
  const [value, setValue] = React.useState(() => _.random(1, 100));

  return (
    <div>
      <button onClick={() => setValue(c => c + 1)}>{value}</button>
    </div>
  );
}

נראה ממש דומה לא?

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

ולמה ההבדל חשוב? בגירסת הקומפוננטה הראשונה כל פעם שאנחנו מעלים את הערך באמצעות לחיצה על הכפתור ריאקט יקרא שוב לפונקציית הקומפוננטה כדי לקבל את ה Virtual DOM Tree המעודכן שלה. קריאה כזו תפעיל גם את _.random, תיקח את התוצאה ותעביר אותה ל useState. עכשיו בגלל שהקומפוננטה כבר נוצרה אז useState פשוט יזרוק לפח את הפרמטר שקיבל בגלל שכבר יש לו ערך בסטייט ויוצא שסתם התאמצנו להגריל מספר אקראי.

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

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

צריכה להיות דרך ברורה אחת (ועדיף שתהיה היחידה) לעשות את זה

23/07/2020

המשפט בכותרת הוא תרגום של המשפט הבא מתוך ה Zen של פייתון. זה המקור:

There should be one-- and preferably only one --obvious way to do it.

והוא נכון לכל השפות. נזכרתי בו במקרה כשדפדפתי בקוד הבא, דווקא בשפת רובי:

class ShoppingCart < ActiveRecord::Base
  has_many :products, :class_name => 'CartProduct', :dependent => :delete_all

    def <<(product)
        line = CartProduct.find_or_initialize_by(:product => product, :cart => self)
        # comment out to allow quantity
        # line.increment(:qty) unless line.new_record?
        line.save!
        @finalized = false

        self
    rescue ActiveRecord::RecordNotUnique
        retry
    end

    def add_with_options(product, options)
        line = CartProduct.find_or_initialize_by(:product => product, :cart => self)
        line.price = options[:price]
        line.options = options
        line.save!
        @finalized = false

        self
    rescue ActiveRecord::RecordNotUnique
        retry
    end
end

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

הבעיה? עכשיו יש לנו שתי דרכים להוסיף מוצר לעגלת הקניות - וקל מאוד לטעות ולשכוח את זה למשל כשכותבים בדיקה:

  test 'cart price is the sum of all product prices' do
    @cart = create(:cart)
    @cart.products << create(:item, price: 20)
    @cart.products << create(:item, price: 50)

    assert_equal(70, @cart.price)
  end

הבדיקה נראית יפה אבל לא שווה כלום אם רוב הקוד במערכת משתמש בשיטה השניה כדי להוסיף מוצרים.

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

תיאורטית פיתרון קל מאוד במקרה כזה יהיה פשוט שהפונקציה הראשונה תקרא לפונקציה השניה, כלומר:

    def <<(product)
        add_with_options(product, {})
    end

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

במקרה שלנו אפשר לחשוב על:

class ShoppingCart < ActiveRecord::Base
    def add_product_sold_by_partner(product, partner_price, partner_options)
    end

    def add_product_from_website(product)
    end
end

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

ריאקט, קלט מספרי ועיקרון ההפתעה הקטנה ביותר

22/07/2020

אחד התלמידים בקורס ריאקט השבוע הופתע לגלות את הטיפול של ריאקט במספרים עם נקודות בתוך שדות קלט מספריים. שימו לב לקומפוננטה הבאה:

function App() {
  const [text, setText] = React.useState('');

  return (
    <div>
      <input type="number" value={text} onChange={e => setText(e.target.value)} />
      <p>Text: {text}</p>      
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('main'));

ויש גם קודפן שתוכלו לשחק עם זה כאן בלי לצאת מהפוסט:

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

התעלמות זו יוצרת אפקט נחמד כשמנסים להקליד מספר עם נקודה - לדוגמא כשניסינו להקליד 3.5 אז ריאקט כותב את ה-3, לא עושה כלום כשכתבתי את הנקודה ואז כותב את ה 3.5 כשהוספתי את ה-5. נסו את זה כי יותר קל להבין כשרואים.

בשביל להבין מה קורה המשכנו לכתוב דוגמא קצת יותר מתוחכמת:


function App() {
  const [numericInputChanges, setNumericInputChanges] = React.useState(0);
  const [textInputChanges, setTextInputChanges] = React.useState(0);
  const [numericInput, setNumericInput] = React.useState('');
  const [textInput, setTextInput] = React.useState('');

  function reset() {
    setTextInput('');
    setNumericInput('');
    setTextInputChanges(0);
    setNumericInputChanges(0);    
  }

  function numericInputChanged(e) {
    setNumericInputChanges(v => v + 1);    
    setNumericInput(e.target.value);
  }

  function textInputChanged(e) {
    setTextInputChanges(v => v + 1);
    setTextInput(e.target.value);
  }

  return (
    <div>
      <p>Numeric Input Changes: {numericInputChanges}</p>
      <p>Text Input Changes: {textInputChanges}</p>      
      <input type="text" value={textInput} onChange={textInputChanged} />
      <input type="number" value={numericInput} onChange={numericInputChanged}  />
      <button type="reset" onClick={reset} >Reset</button>
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('main'));

וכן, יש גם קודפן:

הפעם אני מציג גם Counter שעולה ב-1 כל פעם שקומפוננטת ה input מפעילה את הפונקציה שכתובה ב onChange. הנה מה שקורה שם:

  1. כל פעם שטקסט ב input משתנה, ריאקט מנסה להפוך את הטקסט למספר ואז משווה אותו לערך הקודם שהיה רשום בתיבה.

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

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

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

עריכה - ותודה לגדי על התיקון

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

שעה כל יום

21/07/2020

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

אבל שעה כל יום זה ממש אחלה. בשעה כל יום אפשר ללמוד Data Science או Web Development או אפילו גרמנית. שעה כל יום קונה לכם עולם ומלואו.

והאתגר? להמשיך להשקיע שעה ביום גם אחרי שההתלהבות יורדת, גם אחרי שכבר לא ממש מתחשק ללמוד Data Science או Web Development או מה שלא יהיה. גם אחרי שמצאתם עבודה אחרת (כי בכל זאת צריך לשלם את החשבונות) או שנכנסו 4 פרויקטים חדשים בדיוק באותו יום.

שעה ביום. אל מול עולם שלם שרק רוצה שתוותרו.

מה אתם מחליטים?