• בלוג
  • בואו נבנה את React.lazy כדי להבין איך הוא עובד

בואו נבנה את React.lazy כדי להבין איך הוא עובד

13/01/2020

הפונקציה React.lazy משמשת לטעינה עצלה של פקדי ריאקט, כלומר היא תאפשר לנו בצורה מאוד קלה להגדיר שפקד ריאקט מסוים לא ייכלל בקובץ ה JS הגדול של העמוד וייטען רק אם מישהו ינסה לעשות לו render. דוגמא פשוטה לשימוש בה אפשר למצוא בתיעוד והיא נראית כך:

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

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

קסם? לא ממש. רוב מה ש React.lazy עושה אנחנו יכולים לעשות נפלא גם בלעדיו - בואו נראה איך.

1. צעד 1: מימוש ספציפי עבור פקד Header

נתחיל עם פקד בשם Header שנשמר בקובץ נפרד ופקד ראשי שטוען אותו, כלומר קוד שנראה כך:

import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";
import Header from './header';

export default function App() {
  return (
    <div className="App">
      <Header />
    </div>
  );
}

ברור שכרגע כל הקוד נטען עם עליית העמוד ו Header הוא חלק בלתי נפרד מהפקד הראשי שלנו. נתחיל בשינוי הקוד כך שישתמש ב import דינמי. ב Webpack אפשר להפעיל את import בתור פונקציה. הפעלה כזו נותנת לנו פונקציה שמחזירה Promise, וכשה Promise יתממש אנחנו נקבל אוביקט מודול. לאוביקט המודול יש שדה בשם default שמייצג את ה Default Export של המודול - במקרה שלנו את קומפוננטת ה Header.

לכן צעד ראשון יכול להיראות כך:

import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";

function LazyHeader(props) {
  const [Header, setHeader] = useState(null);
  const fallbackContent = <p>Loading Pleade Wait</p>;

  useEffect(function() {
    import("./header").then(function(module) {
      setHeader(() => module.default);
    });
  }, []);

  if (Header) {
    return <Header {...props} />;
  } else {
    return fallbackContent;
  }
}

export default function App() {
  return (
    <div className="App">
      <LazyHeader />
    </div>
  );
}

ואפשר לראות אותו עובד בקודסנדבוקס בקישור הבא: https://codesandbox.io/s/dazzling-mcclintock-t0xw8

2. צעד שני: מימוש גנרי באמצעות פונקציה

כמובן שהפקד LazyHeader הוא ספציפי מדי לבעיה שלנו: הוא טוען תמיד את אותו מודול מהקובץ header שנמצא איתי באותה תיקיה. הרבה יותר מועיל יהיה לתת לקוד החיצוני להחליט איך טוענים מודול - ולכן נעביר את הקוד לפונקציה שמקבלת כפרמטר "איך לטעון מודול" ומחזירה את הקומפוננטה LazyHeader, רק שהפעם כבר אפשר יהיה לקרוא לה פשוט Lazy.

הבחירה לקבל פונקציית טעינה במקום פשוט שם קובץ נועדה בשביל לאפשר לקוד להיות יותר גנרי. יום אחד אולי מישהו ירצה להשתמש במנגנון טעינה אחר מ Webpack ואנחנו רוצים שהקוד של lazy ימשיך לעבוד.

הקוד אחרי השינוי יראה כך:

import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";

function lazy(componentLoaderFunction) {
  function Lazy(props) {
    const [Component, setComponent] = useState(null);
    const fallbackContent = <p>Loading Pleade Wait</p>;

    useEffect(function() {
      componentLoaderFunction().then(function(module) {
        setComponent(() => module.default);
      });
    }, []);

    if (Component) {
      return <Component {...props} />;
    } else {
      return fallbackContent;
    }
  }

  return Lazy;
}

const LazyHeader = lazy(() => import("./header"));

export default function App() {
  return (
    <div className="App">
      <LazyHeader />
    </div>
  );
}

ואפשר לראות אותו בקודסנדבוקס בקישור הבא: https://codesandbox.io/s/hopeful-euclid-306xy

3. צעד שלישי: תוכן גיבוי

הדבר האחרון ש lazy של ריאקט עושה הוא לאפשר לנו להגדיר כל Fallback Content שנרצה. שימו לב שבקוד שלנו עד שהקומפוננטה נטענת מוצגת הודעה קבועה Loading Pleade Wait, אבל ברור שבעולם האמיתי אנשים ירצו להשתמש בכל מיני ספינרים או אנימציות או לפחות לתרגם את ההודעה לשפות אחרות. בקיצור היינו שמחים שהודעה זו תגיע מבחוץ.

אבל כשאנחנו אומרים מבחוץ למה אנחנו מתכוונים? מאיפה היא צריכה להגיע?

אפשרות אחת היא לקבל את ההודעה כפרמטר נוסף לפונקציה lazy, אבל זו לא אפשרות טובה במיוחד. סיכוי טוב שבמערכת גדולה לא נרצה כל פעם שמפעילים lazy לציין מחדש את קומפוננטת ה Fallback, ונעדיף לכתוב אותה במקום אחד איפשהו במעלה עץ הפקדים (קצת כמו טיפול בשגיאות). זה בדיוק המבנה שראינו עם Suspense. בשביל לממש את המבנה נשתמש ב Context כך ש Suspense ישמור את ה Fallback Content בתוך הקונטקסט, ו Lazy יטען משם את התוכן שצריך להציג.

בקוד זה נראה כך:

import React from "react";
import { useEffect, useState, useContext } from "react";
import "./styles.css";

const FallbackContentContext = React.createContext(null);

function Suspense(props) {
  return (
    <FallbackContentContext.Provider value={props.fallback}>
      {props.children}
    </FallbackContentContext.Provider>
  );
}

function lazy(componentLoaderFunction) {
  function Lazy(props) {
    const [Component, setComponent] = useState(null);
    const fallbackContent = useContext(FallbackContentContext);
    if (!fallbackContent) {
      throw new Error("Missing Fallback Content");
    }

    useEffect(function() {
      componentLoaderFunction().then(function(module) {
        setComponent(() => module.default);
      });
    }, []);

    if (Header) {
      return <Component {...props} />;
    } else {
      return fallbackContent;
    }
  }

  return Lazy;
}

const Header = lazy(() => import("./header"));

export default function App() {
  return (
    <div className="App">
      <Suspense fallback={<p>Loading Please wait</p>}>
        <Header />
      </Suspense>
    </div>
  );
}

וגם גירסא זו זמינה בקודסנדבוקס בקישור: https://codesandbox.io/s/dazzling-lehmann-cgbw0

הצלחנו להגיע למבנה של Lazy ו Suspense בלי עזרה של ריאקט! זה לא רע לפוסט קצר בשלושה צעדים. אני מקווה שסקירה זו עזרה לכם קצת להבין איך Lazy Loading בריאקט עובד. כמובן שבעולם האמיתי מומלץ להשתמש ב React.lazy עצמה כי היא משולבת בתוך קוד הרינדור של ריאקט ונותנת ביצועים טובים יותר.