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

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

לא רוצה באגים בחלק הזה

23/05/2024

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

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

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

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

גישה שנייה מעניינת היא זאת של shadcn - אנחנו עושים קוד בשביל שתעתיק אותו אליך לפרויקט. הסיסמה שלהם - The code is yours.

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

היום למדתי: קונפליקטים באוביקט style בריאקט

22/05/2024

חשבתם פעם מה קורה כשמגדירים גם background וגם background-color ב inline style על קומפוננטה? נו, גם אני לא. עד שנתקלתי בבאג מוזר שדברים פתאום איבדו עיצוב. קודפן להמחשה:

https://codepen.io/ynonp/pen/jOoqKGx

וזה הקוד:

import React from 'https://esm.sh/react@18.2.0'
import ReactDOM from 'https://esm.sh/react-dom@18.2.0'
const baseStyle = { backgroundColor: 'blue', color: 'white', width: '100%' };

const App = () => {
  const [clicked, setClicked] = React.useState(false);
  const style = clicked
    ? Object.assign({}, baseStyle, { background: 'red' })
    : baseStyle;

  return(
    <div className="box"
      style={style}
      onClick={() => {
        setClicked(v => !v);
      }}
     >
       {JSON.stringify([style, clicked])}
     </div>
  );
}

ReactDOM.render(<App />,
document.getElementById("root"))

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

אלא שזה לא מה שריאקט רואה.

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

כמו תמיד אפשר לתקן את זה עם key שונה לשני המצבים, אבל זה לא מה שרצינו שם. ה Takeaway המרכזי כאן הוא לשים לב לא לערבב background עם background-color, ובכל מקרה עדיף להשתמש ב tailwind ולא להיכנס לפינות האלה.

אבולוציה

21/05/2024

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

אחרי זה עברנו ליישומי JavaScript שפנו לשרת ב Ajax ובנו את העמוד מהמידע שחזר בצורה דינמית. במקרה המהפך הזה קרה יחד עם הכניסה של Mobile Apps כי גם הם היו צריכים להוציא מידע מ API אבל הציגו אותו בצורה אחרת.

אחרי זה ראינו שכל לקוח צריך מידע אחר ושיש עלות בפיתוח עוד Endpoint ויש גם עלות בשליחת שאילתות ליותר מדי Endpoints. כלים כמו GraphQL או לינק נוצרו כדי להעביר את משקל הביצועים חזרה לצד השרת תוך שמירת הלוגיקה בצד הלקוח.

והיום יש לנו את next ו Server Components ושוב חזרנו לפנות ל DB מתוך קוד הטמפלייטס ולבנות דפי HTML בעזרת שפה עם סימנים מוזרים, רק שהפעם זה סוגריים מסולסלים במקום סימני שאלה.

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

טיפ טעינת תמונות בריאקט עם ובלי מפתח

20/05/2024

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

function App() {
  const [image, setImage] = React.useState(0);
  return (
    <div>
      <div>
        <button onClick={() => setImage(0)}>1</button>
        <button onClick={() => setImage(1)}>2</button>
        <button onClick={() => setImage(2)}>3</button>
        <button onClick={() => setImage(3)}>4</button>
      </div>
      <img src={images[image]} />
    </div>
  )
}

ככה זה נראה לייב בקודפן:

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

מה קורה פה?

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

איך מתקנים?

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

function App() {
  const [image, setImage] = React.useState(0);
  return (
    <div>
      <div>
        <button onClick={() => setImage(0)}>1</button>
        <button onClick={() => setImage(1)}>2</button>
        <button onClick={() => setImage(2)}>3</button>
        <button onClick={() => setImage(3)}>4</button>
      </div>
      <img key={image} src={images[image]} />
    </div>
  )
}

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

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

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

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

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

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

להסתכל מסביב

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).

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

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

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