• בלוג
  • על החשיבות של useEffect בפיתוח ריאקט

על החשיבות של useEffect בפיתוח ריאקט

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

1. איך העולם נראה לפני

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

  1. רצינו להפעיל קוד מיד אחרי שהקומפוננטה נכנסה למסך (לדוגמה להפעיל טיימר או לפנות לשרת מרוחק למשוך מידע).

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

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

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

class SWCharacter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: null };
  }

  async componentDidMount() {
    const { id } = this.props;
    const response = await fetch(`https://swapi.dev/api/people/${id}/`);
    const data = await response.json();
    this.setState({ name: data.name });
  }

  render() {
    const { id } = this.props;
    const { name } = this.state;
    if (!name) {
      return <p>Loading...</p>;
    }

    return (
      <p>
        User {id}, name = {name}
      </p>
    );
  }
}

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

class SWCharacter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: null };
  }

  async componentDidMount() {
    const { id } = this.props;
    const response = await fetch(`https://swapi.dev/api/people/${id}/`);
    const data = await response.json();
    if (this.props.id === id) {
      this.setState({ name: data.name });
    }
  }

  async componentDidUpdate(prevProps, prevState) {
    if (prevProps.id !== this.props.id) {
      const { id } = this.props;
      const response = await fetch(`https://swapi.dev/api/people/${id}/`);
      const data = await response.json();
      if (this.props.id === id) {
        this.setState({ name: data.name });
      }
    }
  }

  render() {
    const { id } = this.props;
    const { name } = this.state;
    if (!name) {
      return <p>Loading...</p>;
    }

    return (
      <p>
        User {id}, name = {name}
      </p>
    );
  }
}

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

2. המעבר לחשיבה על Side Effects

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

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

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

function BetterSWCharacter({ id }) {
  const [name, setName] = useState(null);

  useEffect(
    function () {
      let activeRequest = true;
      fetch(`https://swapi.dev/api/people/${id}/`)
        .then(function (resp) {
          return resp.json();
        })
        .then(function (data) {
          if (activeRequest) {
            setName(data.name);
          }
        });
      return function () {
        activeRequest = false;
      };
    },
    [id]
  );

  if (!name) {
    return <p>Loading...</p>;
  }

  return (
    <p>
      User {id}, Name = {name}
    </p>
  );
}

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

3. מה עדיין מבלבל

הקמפוננטה הזאת מציגה מספר שמתקדם אחרי שניה ואז מפסיק להתקדם:

function Confusingtimer(props) {
  const [val, setVal] = useState(0);

  useEffect(function () {
    const timer = setInterval(() => {
      setVal(val + 1);
    }, 1000);
    return function () {
      clearInterval(timer);
    };
  }, []);

  return <p>Value = {val}</p>;
}

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

https://overreacted.io/making-setinterval-declarative-with-react-hooks/

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

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