דוגמת ריפקטורינג ב React עם Custom React Hook

13/11/2019

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

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

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

import React from 'react';
import ReactDOM from 'react-dom';
import { useState, useEffect } from 'react';
import _ from 'lodash';
import $ from 'jquery';

const App = () => {
  const [id, setId] = useState(1);
  const [character, setCharacter] = useState({});
  const [filmId, setFilmId] = useState(1);
  const [film, setFilm] = useState({});

  useEffect(function() {
    setFilm({});
    const req = $.getJSON(`https://swapi.co/api/films/${filmId}/`, function(data) {
      setFilm(data);
    });
    return function cancel() {
      req.abort();
    }

  }, [filmId]);

  useEffect(function() {
    setCharacter({});
    const req = $.getJSON(`https://swapi.co/api/people/${id}/`, function(data) {
      setCharacter(data);
    });
    return function cancel() {
      req.abort();
    }
  }, [id]);

  const characterNameText = (character.name == null ?
    '[Loading please wait]' :
    character.name);

  const filmNameText = (film.title == null ?
    '[Loading please wait]' :
    film.title);

  const filmIds = (character.films != null ?
    character.films.map(f => f.match(/(\d+)\/$/)[1]) :
    _.range(1, 8));

  return (
    <div>
      <h1>ID: {id}</h1>
      Character ID:
      <select onChange={(e) => setId(e.target.value)}>
        {_.range(1, 11).map(id => (
          <option value={id}>{id}</option>
        ))}
      </select>
      Film ID :
      <select onChange={(e) => setFilmId(e.target.value)}>
        {filmIds.map(id => (
          <option value={id}>{id}</option>
        ))}
      </select>
      <hr />
      <h2>Character Name: {characterNameText}</h2>
      <h2>Film Name: {filmNameText}</h2>
    </div>
  )
};


const root = document.querySelector('main');
ReactDOM.render(<App />, root);

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

איך מתקנים? כל מה שצריך הוא להוציא את הקוד הכפול לפונקציה:

export function useRemoteData(endpoint) {
  const [id, setId] = useState(1);
  const [data, setData] = useState({});

  useEffect(function() {
    setData({});
    const req = $.getJSON(`https://swapi.co/api/${endpoint}/${id}/`, function(data) {
      setData(data);
    });
    return function cancel() {
      req.abort();
    }

  }, [id]);

  return [id, setId, data];
}

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

const App = () => {
  const [characterId, setCharacterId, character] = useRemoteData('people');
  const [filmId, setFilmId, film] = useRemoteData('films');
  // ...

וכן לריאקט ממש לא אכפת שאתם קוראים מפונקציית הפקד לפונקציה נוספת והיא זו שקוראת בפועל ל useEffect ו useState. למעשה האפשרות הזו להוציא קוד החוצה היא הסיבה המרכזית למעבר ל React Hooks. הפונקציה useRemoteData נקראת Custom Hook וזה כל מה שצריך לדעת בשביל לשתף קוד מבוסס סטייט בין קומפוננטות שונות או לבצע שימוש חוזר בקוד כזה בתוך אותה קומפוננטה.