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

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

טיפ ריאקט: איך לא להשתמש ב 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 כן יכול לתת שיפור מורגש בזמני התגובה.

היכרות עם Redux Toolkit

27/06/2022

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

המשך קריאה

אני לא מאמין ששוב טעיתי ב useEffect

31/05/2022

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

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

import React, { useEffect } from 'react';

// trackedRef is a reference to a DOM element
export default function ResizeHandler({ trackedRef }) {
  useEffect(() => {
    const resizeObserver = new ResizeObserver(() => {
      const height = trackedRef.current.scrollHeight;
      console.log(`Your new height is: ${height}`);
    });
    resizeObserver.observe(trackedRef.current);

    return function cancel() {
      resizeObserver.disconnect();
    }
  }, [trackedRef]);

  return <></>;
}

רואים את הבאג? מוזמנים לשתף בתגובות ולהיזהר מסכינים חדות.

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

היום למדתי: מה באמת הפריע כל כך ב class ב React

27/05/2022

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

Warning: Invalid DOM property `class`. Did you mean `className`?

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

import "./styles.css";

export default function App() {
  return (
    <div class="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

ל div המרכזי יהיה את הקלאס App.

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

export default function App(props) {
    const { class } = props;
}

שלא היה עובד בגלל ש class זו מילה שמורה.

וכן ברור שהיינו יכולים לכתוב במקום משהו כזה:

export default function App(props) {
    const { class: className } = props;
    // now I use className inside the component, to handle the "class" from outside
}

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

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

בואו נכתוב מסוף עם Python, React ו MobX

02/05/2022

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

המשך קריאה

מה זה Tearing ב React ולמה שיהיה לכם אכפת

01/05/2022

גירסה 18 של ריאקט הכניסה לשימוש מנגנון חדש שנקרא Concurrent Mode. אם אתם בונים אפליקציית ריאקט חדשה ב Vite או create-react-app, המנגנון מופעל כברירת מחדל. אם אתם משדרגים אפליקציה ישנה לריאקט 18 אתם תקבלו הודעה שמבקשת מכם להחליף את פקודת ה ReactDOM.render בפקודה בשם ReactDOM.createRoot כדי להפעיל את המנגנון. מנגנון ה Concurrent Mode אמור לעזור לפתור בעיות ביצועים שנובעות מ render-ים ארוכים מדי.

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

את המושג tearing בהקשר של בעיית UI לא מצאתי בתיעוד של ריאקט או בהכרזה על ריאקט 18, אלא רק מפוסטים אחרים שדיברו על הבעיה והרצאה מצוינת ביוטיוב של Daishi Kato. אם יש לכם 20 דקות פנויות שווה להקשיב לו: https://www.youtube.com/watch?v=oPfSC5bQPR8.

בחזרה ל Tearing - בהקשר של ריאקט המושג מתאר מצב בו Concurrent Mode גורם ל UI להיות לא קונסיסטנטי בגלל שינוי משתנים גלובאליים. הבעיה היא כזאת:

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

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

  3. תוך כדי ה render היותר דחוף, הערך של המשתנה הגלובאלי משתנה.

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

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

דאישי הראה בהרצאה דוגמה מעניינת, אחרי שצמצמתי אותה קצת הגעתי לקוד הזה שממחיש בדיוק את הבעיה:

import { useState, useTransition } from 'react'

let lastMouseX = 0;

window.addEventListener('mousemove', (e) => {
  lastMouseX = e.offsetX;
});

function MouseTracker() {
  const start = performance.now();
  while (performance.now() - start < 20);

  return (
    <p>{lastMouseX}</p>
  );
}

function App() {
  const [count, setCount] = useState(0)
  const [isPending, startTransition] = useTransition();

  return (
    <div className="App">
      <p>isPending = {JSON.stringify(isPending)}</p>
      <button onClick={() => {
        startTransition(() => {
          setCount(c => c + 1)
        });
      }}>{count}</button>
      <MouseTracker />
      <MouseTracker />
      <MouseTracker />
      <MouseTracker />
      <MouseTracker />
      <MouseTracker />
    </div>
  )
}

export default App

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

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

בגירסא 18 של ריאקט לפני Concurrent Mode, או אם לא קוראים ל startTransition, ריאקט מבצע את ה render "במכה אחת" כמו בגירסאות הישנות, ושוב לדפדפן אין הזדמנות לשנות את הערך של המשתנה הגלובאלי בין רינדור של קומפוננטות.

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

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

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

מובאקס - מעבר לבייסיקס

29/04/2022

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

  1. מובאקס בעשר דקות

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

  3. וובינר מבוא למובאקס

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

המשך קריאה

מדריך קוד: בואו נכתוב משחק זיכרון ב Redux ו React

06/04/2022

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

המשך קריאה

מהו useDeferredValue ולמה שיהיה לכם אכפת

03/04/2022

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

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

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

הקוד הבא ממחיש את ההתנהגות הרצויה תוך שימוש ב useDeferredValue:

export default function App() {
  const [search, setSearch] = useState("");
  const dsearch = useDeferredValue(search, { timeoutMs: 10000 });
  return (
    <div className="App">
      <input
        type="search"
        value={search}
        onChange={(e) => setSearch(e.target.value)}
      />
      <FilteredList search={dsearch} isPending={dsearch !== search} />
    </div>
  );
}

ואפשר לראות אותו בפעולה בקודסנדבוקס הבא:

קצת הסברים על הקוד:

  1. הסטייט search מייצג את הטקסט בתיבה; המשתנה dsearch הוא עותק של search שהאלמנטים שתלויים בו פחות חשובים.

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

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

חידת ריאקט: קומפוננטות, אפקטים וסדר פעולות

09/08/2021

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

import { useEffect, useState } from "react";

export default function Page(props) {
  useEffect(function () {
    document.title = "Page";
  }, []);

  return <Content />;
}

function Content(props) {
  const [title, setTitle] = useState(document.title);

  useEffect(function () {
    setTitle(document.title);
  }, []);

  return <p>Page title = {title}</p>;
}

והשאלות:

  1. איזה אפקט ירוץ קודם?

  2. מה יהיה תוכן העמוד אחרי רינדור הקומפוננטה Page?

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