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

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

הזמנה לוובינר - חידושים בריאקט 18

03/07/2022

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

  1. נראה מהו useDeferredValue, איזה בעיה הוא בא לפתור ואיזה בעיות הוא יוצר.

  2. ממנו נגיע לדבר על Concurrent Rendering, שזה הפיצ'ר שמאפשר את useDeferredValue ונעבור על הדוגמה של useTransition כדי לראות עוד ייתרון של הרינדור המקבילי.

  3. נדבר על הקושי בעבודה עם רינדור מקבילי, ונראה את הדוגמה של Render Tearing.

  4. נדבר על ההתנהגות החדשה של Strict Mode שעלולה לבלבל במצב פיתוח, ועל השינוי בהתנהגות של useEffect שוב בעקבות ה Concurrent Rendering.

הוובינר יעבור בזום ביום חמישי בעשר בבוקר. הקישור לפרטים והרשמה הוא: https://www.tocode.co.il/workshops/117

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

נתראה בחמישי, ינון

ריאקט 18 סוף סוף מציע פיתרון לבעיית ה label-ים

02/07/2022

עד שהגיע ריאקט או בכלל הרעיון של קומפוננטות, מתכנתים ומתכנתות כתבו קבצי HTML גדולים ובתוכם היו label-ים ו input-ים המתואמים ביניהם באמצעות id:

<label for="name">User Name</label>
<input type="text" id="name" />

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

<label>
User Name
<input type="text" />
</label>

ועד דברים יותר מתוחכמים כמו המצאת מזהים:

function MyForm() {
    const id = Math.random().toString(16);
    return (
        <>
            <label htmlFor={id}>User Name</label>
            <input type="text" id={id} />
        </>
    );
}

הבעיה בפיתרון הראשון היא שהוא מכריח markup מסוים. כל עוד זה עובד לכם הכל טוב, אבל לפעמים באמת ה label וה input לא צמודים אחד לשני. הבעיה בפיתרון השני היא שכנראה לא נקבל את אותו id ב Server Side Rendering, מה שייצור אי תאימות כשריאקט יריץ את כל ה render-ים בדפדפן ויקבל HTML עם מזהים שונים.

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

import { useId } from 'react';

function MyForm() {
    const id = useId();
    return (
        <>
            <label htmlFor={id}>User Name</label>
            <input type="text" id={id} />
        </>
    );
}

ה id שמקבלים לא יצירתי במיוחד ובדוגמה שניסיתי קיבלתי מחרוזת כמו :r1:, :r3 ו :r5:. אני לא יודע למה דווקא r ולמה הוא מדלג על הזוגיים. אם יש לכם רעיון או מידע פנימי אשמח לשמוע בתגובות.

מודל מנטאלי

01/07/2022

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

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

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

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

היום למדתי: הקשר בין globals לניקוי ה DOM ב vitest

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

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "jsdom",
  },
})

אז בכל קבצי הבדיקות שלכם תוכלו להסתמך על זה שיש describe, it ו expect ואולי עוד כמה משתנים גלובאליים. ברירת המחדל היא false ואז צריך לייבא הכל לבד עם שורה כזאת בתחילת קובץ בדיקות:

import { describe, it, expect } from 'vitest';

עד לפה אין סיבה להתרגש, אבל מסתבר שלהזרקת ה globals יש עוד אפקט והוא הרבה יותר מורגש: בעת בדיקת קומפוננטות ריאקט עם react-testing-library, הספריה תנקה את ה DOM בין בדיקה לבדיקה אם ה globals מוזרקים, אבל לא תנקה בלעדיהם.

במילים אחרות בקובץ בדיקות כזה:

import { describe, it, expect } from 'vitest';
import { screen, render } from '@testing-library/react';
import App from './App';

describe('App Changes Colors', () => {
  it('shows a button', () => {
    render(<App />);
    const btn = screen.getByRole('button', { name: '0' });
    expect(btn).toBeTruthy();
  });
  it('shows a button', () => {
    render(<App />);
    const btn = screen.getByRole('button', { name: '0' });
    expect(btn).toBeTruthy();
  });
});

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

הפונקציה הרלוונטית מ testing-library שמטפלת בסיפור המחיקה נקראת cleanup וזה מה שמוסבר גם בתיעוד שלה:

    Please note that this is done automatically if the testing framework you're using supports the afterEach global and it is injected to your testing environment (like mocha, Jest, and Jasmine). If not, you will need to do manual cleanups after each test.

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

טיפ ריאקט: איך לא להשתמש ב useDeferred

29/06/2022

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

נתבונן בקוד הבא:

function BrokenHugeList() {
  const [search, setSearch] = useState('');
  const deferredSearch = useDeferredValue(search, { timeoutMs: 5000 });

  function handleChange(e) {
    setSearch(e.target.value);
  }
  console.log(`1 seach = ${deferredSearch}`);
  const searchResults = items.filter(i => i.includes(deferredSearch));
  console.log('2');

  return (
    <div>
      <input type="text" value={search} onChange={handleChange} />
      <ul>
        {searchResults.map(i => <li key={i}>{i}</li>)}
      </ul>
    </div>
  );
}

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

וזה באמת מה שקורה רק שצריך לזכור שתי נקודות שבסוף יכולות לפעול לרעתנו:

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

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

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

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

const HugeList = React.memo(function HugeList(props) {
  const { search } = props;
  console.log(`3, search = ${search}`);
  const searchResults = items.filter(i => i.includes(search));
  console.log('4');

  return (
    <div>
      <ul>
        {searchResults.map(i => <li key={i}>{i}</li>)}
      </ul>
    </div>
  );
});

function App() {
  const [search, setSearch] = useState('');
  const deferredSearch = useDeferredValue(search, { timeoutMs: 5000 });

  function handleChange(e) {
    setSearch(e.target.value);
  }

  return (
    <>
      <input type="text" value={search} onChange={handleChange} /> 
      <HugeList search={deferredSearch} />
      <hr />
    </>
  );
}

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

התמונה הגדולה

28/06/2022

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

אבל הערך האמיתי הוא בתמונה הגדולה - למה בכלל אנחנו בודקים? איזה ערך מקבלים מכל סוג בדיקות? כמה זה עולה לנו?

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

היכרות עם Redux Toolkit

27/06/2022

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

המשך קריאה

המחיר של יצירתיות

26/06/2022

השקענו כבר כל כך הרבה...

כל הלקוחות שלנו מחכים לפיצ'ר הזה...

זה ממש כמעט עובד...


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

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

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


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

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

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

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

לולאות באגים

24/06/2022

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

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

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

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

נקסט!