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

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

הקסם של Suspense בריאקט

27/05/2024

הרבה זמן היה קשה לראות את זה, אבל עכשיו עם Server Components הקומפוננטה Suspense מקבלת כח חדש והופכת להכרחית בהרבה אפליקציות next.js. הסיפור הולך כך:

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

  2. אנחנו רוצים לשלוח ללקוח את העמוד כמה שיותר מהר, וכשיגיעו הנתונים מבסיס הנתונים או מה API "להשלים" אותם בעמוד.

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

הפונקציה use שתיכנס בריאקט 19 תאפשר לשפר קצת את המצב מבחינת אפליקציית צד הלקוח בכך שנוכל לעצור את הרינדור באמצע ולחכות ל Promise (קצת כמו await באמצע קוד קומפוננטה), ואז לא נצטרך לטפל במצב שהקומפוננטה "טוענת". מנגנון Server Components וה Component Streaming מאפשר ללכת עוד צעד קדימה ולטפל בכל הסיפור הזה בצד השרת.

שימו לב לדוגמה הבאה:

import { Suspense } from 'react'; 

/* We've moved data fetching into the changlog compoment */
async function ChangelogWithDataFetching() {
  const changelogData = await getChangelogData()
  return <Changelog data={changelogData} />
}

/*
  ...and wrapped that component in Suspense.
  The user can see the page immediately, while the changelog component loads
*/
export default function Page() {
  return (
    <Layout>
      <Sidebar>
        {/* other sidebar stuff */}
        <Suspense fallback={<ChangelogPlaceholder />}>
          <ChangelogWithDataFetching />
        </Suspense>
      </Sidebar>
      {/* other page stuff */}
    </Layout>
  )
}

קוד הגיע מכאן.

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

תרגיל בריפקטורינג טייפסקריפט

26/05/2024

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

interface Item {
    id: number;
    text: string;
    likes: number;
    price: number;
}

function updateItem(item: Pick<Item, 'id'> & Partial<Item>) {
    return fetch(`/api/update/${item.id}`, {
        method: 'POST',
        headers: { contentType: 'application/json' },
        body: JSON.stringify(item)
    });
}

הפונקציה updateItem מקבלת אחד או יותר מהשדות של Item אבל חייבת לקבל את השדה id ושולחת את המידע לשרת לצורך עדכון.

המשך קריאה

ריאקט, אפקט וספריות משיכת מידע

25/05/2024

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

Data is requested twice in development mode, this is due to using a useEffects in Strict Mode for querying - We’d recommend using a library to help with fetching and checking out this: https://react.dev/learn/you-might-not-need-an-effect

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

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

(והסקרנים שרוצים לגלות מה בדיוק ספריית fetch עושה מוזמנים להציץ במימוש של swr. שימו לב שם ש useIsomorphicLayoutEffect קורא ל useEffect או ל useLayoutEffect).

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

מה יכול להשתבש ב next.js

24/05/2024

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

  1. זזים מהר ושוברים דברים - בגירסה 13 הם יצאו עם רעיון חדש לארכיטקטורה של אפליקציה, בגירסה 14 זה כבר היה ברירת המחדל. בגירסה 14 הם בנו מנגנון Caching מסוים, בגירסה 15 הם כבר שינו חלק מברירות המחדל שלו. גירסה 15 תומכת כבר ב React Compiler שעדיין נמצא בבטא ובאופן כללי נקסט הוא המקום בו כל החידושים של ריאקט קורים. זה נחמד אם אנחנו מוכנים לזוז מהר ולשחק עם טכנולוגיות, זה פחות נחמד שנתיים אחרי כשאנחנו 4 גירסאות מאחור. כרגע הדגש של האנשים שבונים את נקסט הוא פרויקטים חדשים. שימו לב שאתם מוכנים למסע.

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

  3. ה Deployment ל Vercel הוא פינוק, אבל מהר מאוד הם ידחפו אתכם לתוכנית בתשלום ושם יש Lock In מסוים, במיוחד אם אתם משתמשים ב APIs של ורסל כמו בסיס הנתונים או מערכת איחסון הקבצים שלהם.

  4. נקסט לא תומך Deno.

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

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

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

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

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) ולטיו יכול להיות פיתרון מדויק לקצת סטייט גלובאלי שרוצים לשתף בין כל הקומפוננטות במערכת.