עדיפה ארכיטקטורה טובה על מעקף טוב

09/01/2023

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

function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);
  if (prevCount !== count) {
    setPrevCount(count);
    setTrend(count > prevCount ? 'increasing' : 'decreasing');
  }
  return (
    <>
      <h1>{count}</h1>
      {trend && <p>The count is {trend}</p>}
    </>
  );
}

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

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

פיתרון נכון לשמירת מגמה יהיה לשמור את המגמה יחד עם הסטייט המקורי - כלומר מי שמחזיק את count ואחראי על עדכון שלו הוא גם זה שצריך לשמור את המגמה. שמירת זיכרון של הערך הקודם היא תכונה של הסטייט, בדיוק כמו שמירה של משתנה הסטייט ב Local Storage או כל מניפולציה אחרת על הערך. וכך פיתרון עם Custom Hook הולך להיות הרבה יותר פשוט ויותר נכון מההצעה של סטייט נוסף בקומפוננטת ילד. זה נראה ככה:

import { useState } from "react";

function useTrendedState(initialValue) {
  const [value, setValue] = useState(initialValue);
  const [prevValue, setPrevValue] = useState(initialValue);

  function setter(...args) {
    setPrevValue(value);
    return setValue(...args);
  }

  return [value, setter, prevValue];
}

function CountLabel({ count, prevCount }) {
  let trend;
  if (count < prevCount) {
    trend = "Decreasing";
  } else if (count > prevCount) {
    trend = "Increasing";
  } else {
    trend = "Stable";
  }

  return (
    <>
      <h1>{count}</h1>
      {trend && <p>The count is {trend}</p>}
    </>
  );
}

export default function App() {
  const [count, setCount, prevCount] = useTrendedState(0);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <CountLabel count={count} prevCount={prevCount} />
    </>
  );
}

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

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