• בלוג
  • איך סוליד פתר את הבעיה הכי מעצבנת עם useEffect של ריאקט

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

29/03/2022

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

1. דוגמאות לאפקטים

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

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

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

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

  5. קומפוננטה מעדכנת משתנה State כל פעם ש prop מסוים שלה מתעדכן.

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

  1. יש אפקטים שצריכים לקרות גם אחרי ה render הראשון - לדוגמה להפעיל timer או למשוך מידע מ API מרוחק.

  2. יש אפקטים שצריכים לקרות רק כשאחד הערכים שמשפיע על האפקט משתנה - לדוגמה עדכון ה document.title כשמשתנה state משנה את ערכו, או עדכון משתנה state אחרי שקיבלנו ערך חדש ל property מסוים.

הקומפוננטה Ticker הבאה היא דוגמה למקרה הראשון. היא מציגה מספר שעולה ב-1 כל שניה:

function Ticker() {
  const [value, setValue] = useState(0);
  useEffect(() => {
    setInterval(() => {
      setValue(v => v + 1);
    }, 1000);
  }, []);

  return (
    <p>{value}</p>
  )
}

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

export default function TitleEditor() {  
  const [value, setValue] = useState(document.title);
  useEffect(() => {
    console.count(`writing ${value} to document.title`);
    document.title = value;
  }, [value]);

  return (
    <input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
  )
}

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

עכשיו תגידו ובצדק שבדוגמה הפשוטה של TitleEditor מאוד קל לפתור את זה - פשוט בודקים אם document.title זהה ל value שיש לי, ואם כן לא מעדכנים אותו. אבל במקרים יותר מורכבים ייתכן ואנחנו רוצים לסנכרן את המשתנה עם מידע בשרת וגם הקריאה עצמה לוקחת זמן. הדרך המקובלת בריאקט לדלג על האפקט אחרי render ראשוני היא לייצר ref כדי לזכור האם האפקט רץ פעם אחת. אפשר לקרוא על הטכניקה כאן:

https://medium.com/swlh/prevent-useeffects-callback-firing-during-initial-render-the-armchair-critic-f71bc0e03536

2. טריק קטן של סוליד שעושה את ההבדל

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

הקומפוננטה הבאה בסוליד משיגה את אותו אפקט, תוך דילוג על הכתיבה הראשונה (המיותרת) ל document.title:


function App() {
  const [value, setValue] = createSignal(document.title);
  createEffect((prev) => {
    if (prev) {
      document.title = value();
    }

    return value();
  }, null);

  return (
    <input
      type="text"
      value={value()}
      onInput={(e) => setValue(e.currentTarget.value)}
    />
  );
}

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