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

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

אקוסיסטם, יתרונות וחסרונות

19/05/2024

בשביל להעלות פרויקט ל Deno Deploy הוא צריך להיות כתוב ב Deno (פרויקט next שעושה את אותו דבר יעלה לשרתים של Vercel).

הפריימוורק הריאקטי לפיתוח ווב של דינו נקרא fresh (זה המקביל של next). אבל פרש משתמש ב preact במקום בריאקט.

בשביל להוסיף אנימציה לפרויקט ריאקט אני מוסיף את Framer Motion, אבל ספרייה זו לא עובדת טוב עם preact.

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

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

מה עושים? בינתיים אפשר לקטר. יום אחד אולי יגיעו Web Components ויוציאו אותנו מהברוך הזה.

ניסוי Valtio (או: לא מבין את ההתלהבות מזוסטנד)

18/05/2024

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

import { proxy, useSnapshot } from 'valtio'

const state = proxy({ count: 0, text: 'hello' })

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

function Counter() {
  const snap = useSnapshot(state)
  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  )
}

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

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

נראה את זה עובד? בשמחה. בניתי מערך דו מימדי של קופסאות ובכל קופסה יש מספר 1 או 0. כולם מתחילים עם 0, והמערך נשמר במשתנה state:

const state = proxy(new Array(10).fill(0).map(_ => new Array(10).fill(0)))

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

const Box = React.memo((props) => {
  const { i, j } = props;
  console.count(`Box: ${i} / ${j}`);
  const canvas = useSnapshot(state);
  const cell = canvas[i][j];
  
  const handleClick = React.useCallback((e) => {
    state[i][j] = state[i][j] === 0 ? 1 : 0;
  });
  
  return <div style={styleCell(cell)} onClick={handleClick}></div>
})

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

אפשר למצוא את הדוגמה המלאה בקודפן כאן: https://codepen.io/ynonp/pen/rNgOXwK?editors=1010

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

const useStore = create((set) => ({
  data: new Array(10).fill(0).map(_ => new Array(10).fill(0)),
  toggle: (i, j) => set((state) => produce(state, draft => {
      draft.data[i][j] = draft.data[i][j] === 0 ? 1 : 0
  }))
}));

וגם בתוך הקומפוננטה הייתי צריך להתאמץ יותר בשביל להגיע לפונקציה:

const Box = React.memo((props) => {
  const { i, j } = props;
  const cell = useStore((state) => state.data[i][j]);
  const toggle = useStore((state) => state.toggle);

  console.count(`Box: ${i} / ${j}`);

  const handleClick = React.useCallback((e) => {
    toggle(i, j);
  });

  return <div style={styleCell(cell)} onClick={handleClick}></div>
})

זה הקודפן המלא עם זוסטנד: https://codepen.io/ynonp/pen/RwmrNXG?editors=1010

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

להסתכל מסביב

17/05/2024

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

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

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

  1. מתחילים עם ה APIs של הפריימוורק שעדיין לא נגעת בהם. אולי זה Active Storage או Action Cable או Stimulus או Route Constraints. אפשר למצוא כאלה בקלות - פשוט קוראים את התיעוד ומחפשים דברים שעדיין לא מכירים. כדאי לרשום את כולם על פתק.

  2. ממשיכים את הפתק עם ספריות שמשלימות את הטכנולוגיה המרכזית בה אנחנו עובדים. פה ה AI הוא ממש חבר טוב. זה מה שהוא ענה לי כששאלתי "מהן 10 הספריות החשובות ביותר למפתחי ריילס": Devise, Pundit, ActiveAdmin, CarrierWave, Sidekiq, RSpec, Capistrano, Delayed Job, Nokogiri, Rails Admin. צריך להגיד - אני לא מסכים איתו בכל הנקודות. אבל הדבר החשוב כאן זה שאפשר להרחיב את החיפוש בקלות, או לבקש עוד 10 או לגוון בבקשה, למשל השאלה השנייה ששאלתי את ChatGPT היתה:

I'm a Rails developer and I want to expand my knowledge with new techniques while still using Rails. What gems or techniques do you suggest?

ופה כבר קיבלתי ג'מים ממש מעניינים כמו simple_command, wisper, versionist, graphql-ruby, whenever, searchkick, any_cable, bullet, brakeman.

  1. ממשיכים את הפתק עם פריימוורקים אחרים אבל עדיין ברובי, למשל Hanami, Padrino, Ramaze, Sinatra ו Cuba. אני לא יודע עליהם כלום בינתיים. לאט לאט אלמד.

  2. ואז משנים קצת - למשל מנסים כל מיני סוגים של בסיסי נתונים או שירותי איחסון.

(וכל זה בלי שדיברנו על עולם ה Front End).

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

סקרנות

17/05/2024

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

סקרנות היא היכולת לשאול "במה זה שונה". איך ההבדל בין Ruby ל JavaScript משפיע על פרויקטי אקספרס מול סינטרה? איזה סוגי ספריות אנשים בונים לכל פריימוורק? על מה הם שמים דגש?

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

כמה זמן לוקח לכתוב בדיקה חדשה?

16/05/2024

  • כמה זמן לוקח לכתוב בדיקה חדשה?
  • תלוי כמה בדיקות כבר יש לך

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

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

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

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

מעדכנים אוביקט JSON עם JSONPath

15/05/2024

נניח שיש לכם אוביקט JSON מקונן למשל את זה:

const data = {
  rows: [
    {id: '1', cells: [{text: 'a', state: { active: true }},
                      {text: 'b'}]},
    {id: '2', cells: [{text: 'c'}]}
  ]
}

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

for (let row of data.rows) {
    for (let cell of row.cells) {
    if (!cell.state) { cell.state = {} }
        cell.state.active = false
  }
} 

וזה עובד. זמנית. כי מהר מאוד האוביקט יקבל שורה בלי cells למשל:

const data = {
  rows: [
    {id: '1', cells: [{text: 'a', state: { active: true }},
                      {text: 'b'}]},
    {id: '2', cells: [{text: 'c'}]},
    {id: '3' }
  ]
}

ועכשיו לך תזכור איפה הטעות.

גישה יותר גמישה היא JSONPath. זה spec שמאפשר לחפש מידע באוביקט JSON מקונן או לשנות שדות בתוך אותו אוביקט. הספריה jsonpath מ npm כוללת פונקציות לעבודה עם נתיבים בתוך JSON-ים ואפשר לשלב אותה ביישומי node.js או בדפדפן. הדוגמה שלהם מתוך דף התיעוד היא:

var cities = [
  { name: "London", "population": 8615246 },
  { name: "Berlin", "population": 3517424 },
  { name: "Madrid", "population": 3165235 },
  { name: "Rome",   "population": 2870528 }
];

var jp = require('jsonpath');
var names = jp.query(cities, '$..name');

// [ "London", "Berlin", "Madrid", "Rome" ]

המחרוזת $..name היא נתיב לכל הערכים שמתאימים למפתח בשם name בכל עומק של האוביקט. יש להם גם טבלה בדף התיעוד עם כל הסימנים שאפשר לרשום בנתיב JSON. בחזרה לאוביקט שלנו עם השורות והתאים בעזרת JSONPath אפשר לפתור את הבעיה בצורה יותר פשוטה וגנרית:

function desactivateAll() {
  jsonpath.apply(data, '$.rows[*].cells[*]', (v) => v.state ? v : Object.assign(v, {state: { active: false }}))
  // OR less "safe way":
  // jsonpath.apply(data, '$...active', (v) => false);
  jsonpath.apply(data, '$.rows[*].cells[*].state.active', (v) => false);
}

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

כתבתי גם דוגמה מלאה בקודפן בקישור הזה כדי שיהיה לכם קל לשחק עם הספריה: https://codepen.io/ynonp/pen/eYamWYp

למה כל כך קשה לנו עם אחידות בקוד?

14/05/2024

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

קאפ כתוב בטייפסקריפט ומשתמש בכל הספריות המודרניות שהייתם מצפים למצוא בפרויקט היום כמו Tailwind, React וכמובן Drizzle. יש גירסת דסקטופ עם טאורי וגם קוד האתר זמין בגיטהאב. הקוד עצמו פשוט קל לקריאה לדוגמה הקובץ הזה הוא דף ה FAQ שלהם:

"use client";

const faqContent = [
  {
    title: "What is Cap?",
    answer:
      "Cap is an open source alternative to Loom. It's a video messaging tool that allows you to record, edit and share videos in seconds.",
  },
  {
    title: "How do I use it?",
    answer:
      "Simply download the Cap macOS app (or the Cap web app), and start recording. You can record your screen, your camera, or both at once. After your recording finishes, you will receive your shareable Cap link to share with anyone.",
  },
  {
    title: "Who is it for?",
    answer:
      "Cap is for anyone who wants to record and share videos. It's a great tool for creators, educators, marketers, and anyone who wants to communicate more effectively.",
  },
  {
    title: "How much does it cost?",
    answer:
      "Cap is free to use. However, you can upgrade to Cap Pro for just $9/month and unlock unlimited recordings, unlimited recording length, and much more.",
  },
  {
    title: "What makes you different to Loom?",
    answer:
      "Apart from being open source and privacy focused, Cap is also a lot more lightweight and faster to use. We also focus strongly on design and user experience, and our community is at the heart of everything we do.",
  },
];

export const FaqPage = () => {
  return (
    <div className="wrapper wrapper-sm py-20">
      <div className="text-center page-intro mb-14">
        <h1>FAQ</h1>
      </div>
      <div className="mb-10">
        {faqContent.map((section, index) => {
          return (
            <div key={index} className="max-w-2xl mx-auto my-8">
              <h2 className="text-xl mb-2">{section.title}</h2>
              <p className="text-lg">{section.answer}</p>
            </div>
          );
        })}
      </div>
    </div>
  );
};

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

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

    async createUser(userData) {
      await db.insert(users).values({
        id: nanoId(),
        email: userData.email,
        emailVerified: userData.emailVerified,
        name: userData.name,
        image: userData.image,
        activeSpaceId: "",
      });
      const rows = await db
        .select()
        .from(users)
        .where(eq(users.email, userData.email))
        .limit(1);
      const row = rows[0];
      if (!row) throw new Error("User not found");
      return row;
    },
    async getUser(id) {
      const rows = await db
        .select()
        .from(users)
        .where(eq(users.id, id))
        .limit(1);
      const row = rows[0];
      return row ?? null;
    },

הפונקציה createUser תזרוק שגיאה אם היתה בעיה ביצירה. הפונקציה getUser תחזיר null.

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

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

חדש באתר: עדכונים לקורס ריאקט

13/05/2024

הי חברות וחברים,

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

  1. ארגון מחדש של מבנה הפרויקט כך שישתמש ב vite - כשהקלטתי את הקורס במקור לא היה עדיין vite. כן היה את create-react-app אבל לא אהבתי אותו מהיום הראשון שראיתי אותו ושמחתי כשהעולם נפרד ממנו יחסית מהר. בכל מקרה בקורס ריאקט פה לא רציתי להשתמש ב create-react-app ולכן בכל הדוגמאות מבנה הפרויקט השתמש בקובץ הגדרות webpack שכתבתי לבד. ויט מעולה והיום אני כבר לא ממליץ לכתוב לבד הגדרות וובפאק אלא להשתמש רק בו, ולכן ארגנתי מחדש גם את כל מבנה הדוגמאות בקורס ריאקט כך שהתבניות ישתמשו ב vite. זה אומר שגם השיעורים על התקנת ריאקט על המכונה שלכם ואיתור שגיאות בריאקט עודכנו לתבנית החדשה.

  2. הוספתי דף תרגילים מסכם לחלק הראשון של הקורס.

  3. הוספתי שיעור על Error Boundaries - בעבר היה שיעור מקיף יותר על Lifecycle Methods של Class Components שכלל גם התיחסות בקטנה ל Error Boundary, אבל האמת שהיום מכל פונקציות ה Lifecycle המנגנון הכי חשוב (וזה שאין לו אלטרנטיבה עם Hooks) הוא טיפול בשגיאות. בקיצור הקלטתי מחדש את השיעור כך שיתמקד במנגנון תפיסת שגיאות Render וארגון אזורי טיפול בשגיאות.

  4. מחקתי בשמחה את השיעור על create-react-app ואני מקווה לא להיתקל שוב בכלי זה.

רוב השינויים הם בחלק הראשון של הקורס שפתוח לצפיה חינם אז אם חיפשתם הזדמנות לקחת צעדים ראשונים בריאקט מוזמנים להעיף מבט: https://www.tocode.co.il/bundles/react

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

ניסוי: מספרים גדולים ב JavaScript

12/05/2024

הקוד הבא מחשב סכום ספרות של מספר:

function sumOfDigits(n) {
  let sum = 0;
  while (n > 0) {
    sum += n % 10;
    n = Math.floor(n / 10);
  }
  return sum;
}

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

const input = document.querySelector('input');
input.addEventListener('input', (e) => {
  const result = sumOfDigits(Number(e.target.value));
  document.querySelector('#result').textContent = result;
});

אפשר לראות אותו בפעולה בקודפן בקישור: https://codepen.io/ynonp/pen/XWwJJmw?editors=1010

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

מה עושים? עוברים לעבוד עם מספרים שלמים גדולים - BigInt. מספר גדול מאותחל דרך הבנאי BigInt או באמצעות הסיומת n כלומר כל אלה הם מספרים גדולים:

const n1 = BigInt(9007199254740991);
const n2 = BigInt(3);
const n3 = 10n;

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

function sumOfDigits(n) {
  let sum = 0n;
  while (n > 0) {
    sum += n % 10n;
    n = n / 10n;
  }
  return sum;
}

const input = document.querySelector('input');
input.addEventListener('input', (e) => {
  const result = sumOfDigits(BigInt(e.target.value));
  document.querySelector('#result').textContent = result;
});

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

https://codepen.io/ynonp/pen/wvbBBBP?editors=1010

להתאמץ פחות

11/05/2024

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

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