• בלוג
  • איך (ולמה) לכתוב React Hook

איך (ולמה) לכתוב React Hook

17/03/2019

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

ריאקט עברה ועוברת עדיין הרבה טלטלות מבחינת ה API, כאשר אולי הגדולה ביותר היתה ההמלצה להפסיק להשתמש ב React.createClass ולעבור להשתמש במחלקות וב Function Components כדי ליצור את הקומפוננטות.

נודה על האמת הפונקציה React.createClass היתה סוג של פשרה מהיום הראשון. הרי אם היו class-ים ב JavaScript כשריאקט התחילה כנראה לא היו צריכים לכתוב אותה. לפונקציה זו מקבילה בהרבה ספריות UI אחרות ישנות יותר כמו למשל Class.create של פרוטוטייפ או declare של dojo.

אבל משהו מאוד גדול הלך לאיבוד במעבר מ React.createClass ל class המודרני, ולמשהו הזה קוראים Mixins. מיקסינס הפכו במעבר למחלקות למבנה שנקרא Higer Order Components שהיה הרבה פחות אינטואיטיבי לרוב המפתחים. בפוסט היום ניזכר יחד מה היו מיקסינס, איך HoC אמורים היו להחליף אותם ומה Hooks מצליחים לעשות טוב יותר. מוכנים?

1. החיים לפני class

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

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

const WithTimer = {
  getInitialState() {
    return { ticks: 0 };
  },

  componentDidMount() {
    this._withTimer__clock = setInterval(() => {
      this.setState({ ticks: this.state.ticks + 1 });
    }, 1000);
  },

  componentWillUnmount() {
    clearInterval(this._withTimer__clock);
  },
};

המיקסין מגדיר את הפונקציות componentDidMount ו componentWillUnmount וכולל את המימוש שלהן כמו שהיה נכתב בקומפוננטה הרגילה. ממש יכולנו לעשות Copy-Paste מקוד בקומפוננטה שראינו שהופיע במספר מקומות החוצה ל Mixin.

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

const App = React.createClass({  
  getInitialState() { return {} },

  mixins: [WithTimer],

  render() {
    return (
      <div>
        <p>Hello World {this.state.ticks} </p>
      </div>
    );
  }
});

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

2. קומפוננטות מסדר גבוה

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

  1. אי אפשר להשתמש ב Delegation ולהוציא את המנגנון למחלקה שלישית, כי אני לא רוצה מהקומפוננטה שלי לכתוב את componentDidMount ו componentWillUnmount. הייתי רוצה בשורה אחת לקבל את שתיהן.

  2. אי אפשר להשתמש בירושה כי אני רוצה להיות מסוגל לקבל "התנהגות" מכמה סוגים (למשוך כמה מיקסינים), ובירושה זה אומר שהייתי צריך ליצור קשר בין מחלקות הבסיס. במילים אחרות אם אני רוצה את WithTimer וגם את WithAnimation - הייתי צריך לבנות עץ מחלקות בו WithAnimation יורשת מ WithTimer או להיפך, ושתי האופציות לא טובות.

לכן יחד עם המעבר לקלאסים דן אברמוב והצוות התחילו להמליץ על תבנית חדשה שנקראת "קומפוננטות מסדר גבוה" או Higher Order Components. מדובר בפונקציות שמקבלות כקלט קומפוננטה ומחזירות קומפוננטה אחרת. הקומפוננטה האחרת "עוטפת" את הקומפוננטה המקורית ומוסיפה לה התנהגות באמצעות העברת Properties.

זה אומר שעץ הקומפוננטות (ה Virtual DOM) שלנו כבר לא יכיל רק קומפוננטה יחידה App והטיימר בתוכה, אלא בשביל לשתף התנהגות אנחנו צריכים להוסיף עוד קומפוננטה לעץ ולקבל את המבנה הבא:

<WithTimer>
    <App ticks={ticks} />
</WithTimer>

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

function withTimer(Component) {
  return class WithTimer extends React.Component {
    constructor(props) {
      super(props);
      this.state = { ticks: 0 };
    }

    componentDidMount() {
      this.clock = setInterval(() => {
        this.setState(state => ({ ticks: state.ticks + 1 }));
      }, 1000);
    }

    componentWillUnmount() {
      clearInterval(this.clock);
    }

    render() {
      return (
        <Component ticks={this.state.ticks} />
      )
    }
  }
}

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

  1. זה הרבה יותר מסובך להבנה לעומת Mixins.

  2. אנחנו מגדילים את ה Virtual DOM שלא לצורך. כל מה שרציתי היה לשתף התנהגות וממש לא עניין אותי להוסיף עכשיו עוד קומפוננטה.

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

הקוד שמשתמש ב HoC אגב נראה כך:

const App = withTimer(React.createClass({
  render() {
    return (
      <div>
        <p>Hello World {this.props.ticks} </p>
      </div>
    );
  }
}));

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

3. ניסיון שלישי עם Hooks

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

לכן Hook הוא פשוט פונקציה שלישית שאנחנו קוראים לה כדי לשתף לוגיקה בין קומפוננטות. בעבודה עם Function Components ה Hooks יאפשרו שיתוף קוד הרבה יותר אינטואיטיבי בהשוואה למה ש Higher Order Components הציעו כשעבדנו עם מחלקות.

אז איך זה עובד? בריאקט תומכים היום כמעט בכל פונקציות מחזור החיים של קומפוננטה מתוך Function Component באמצעות מספר פונקציות חדשות שנוספו ל API:

  1. הפונקציה useState מאפשרת לשמור State בתוך פקד פונקציה.

  2. הפונקציה useEffect מאפשרת להריץ קוד כשפקד-פונקציה נכנס או יוצא מהעץ, מה שאומר שהיא מחליפה את componentDidMount, componentWillUnmount ואת componentDidUpdate.

  3. הפונקציה useRef מאפשרת להגדיר ref מתוך פקד-פונקציה.

  4. הפונקציה useReducer מאפשרת לכתוב מיני-רדוקס בתוך פקד-פונקציה, כדי לבנות מעין מכונות מצבים.

  5. הפונקציה useMemo עוזרת לבנות מנגנון דומה ל shouldComponentUpdate.

את הרשימה המלאה עם פירוט של Use-Cases ודוגמאות אפשר למצוא בתיעוד.

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

אז מה לגבי ה withTimer שלנו? טוב במעבר ל Hooks הוא הפך ל useTimer כי המלצת השמות שלהם השתנתה, אבל הוא גם נהיה הרבה יותר פשוט:

function useTimer() {
  const [ ticks, setTicks ] = useState(0);
  useInterval(function() {
    setTicks(ticks + 1);
  }, 1000);
  return ticks;
}

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

function App(props) {
  const ticks = useTimer();
  return (
    <div>
      <p>Hello World {ticks}</p>
    </div>
  );
}

נראה מה הרווחנו:

  1. קל מאוד להוסיף התנהגויות נוספות או להוריד התנהגויות, פשוט מוסיפים או מורידים קריאות בגוף הפונקציה.

  2. אין תוספת של קומפוננטות מיותרות ל Virtual DOM - הקומפוננטה היחידה שם היא App, בדיוק כמו בעבודה עם מיקסינס.

  3. אין צורך להשתמש ב bind בשום מקום, בגלל שאין כאן מחלקות. זה אומר שזה המנגנון היחיד שאיפשר לי לכתוב function ולא להשתמש בפונקציית-חץ בתוך ה setInterval.

  4. בניגוד למיקסינס, אין כאן סכנה של התנגשויות. מי שמשתמש ב Hook בוחר את השמות, ולא (כמו במיקסין) מי שכתב את המיקסין.

והקודפן:

כרגע הבעיה היחידה שאני רואה עם Hooks זה התהום שהם פתחו בין קומפוננטות-מסוג-מחלקה לקומפוננטות-מסוג-פונקציה. בניגוד ל Higher Order Components, בעולם של Hooks ברגע שכתבתם את הלוגיקה המשותפת בתור Hook אתם יכולים לשלב אותה אך ורק בתוך קומפוננטות-פונקציה. הדבר מייצר תמריץ חזק להעביר כמה שיותר קומפוננטות למבנה של קומפוננטות-פונקציה בשביל שנוכל לשתף ביניהן קוד בשיטה החדשה והקלה. וכמובן ככל שיותר כותבי סיפריות יעדיפו לכתוב Hooks על פני Higher Order Components כך גם אנחנו המפתחים נצטרך להעביר את הקוד שלנו עוד מיגרציה יקרה רק בשביל שנוכל להנות מספריות ויכולות חדשות.