עברית

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

ארכיטקטורת רכיבי React: מדוע הקומפוזיציה מנצחת את הירושה

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

אם אתה מגיע מרקע של שפות מונחות עצמים קלאסיות כמו Java או C++, ירושה עשויה להרגיש כמו בחירה טבעית ראשונה. זהו מושג רב עוצמה ליצירת קשרים מסוג 'is-a'. עם זאת, תיעוד React הרשמי מציע המלצה ברורה וחזקה: "בפייסבוק, אנו משתמשים ב-React באלפי רכיבים, ולא מצאנו שום מקרי שימוש שבהם היינו ממליצים ליצור היררכיות ירושה של רכיבים."

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

הבנת המשמרת הישנה: מהי ירושה?

ירושה היא עמוד תווך מרכזי של תכנות מונחה עצמים (OOP). היא מאפשרת למחלקה חדשה (התת-מחלקה או הבת) לרכוש את המאפיינים והשיטות של מחלקה קיימת (העל-מחלקה או האם). זה יוצר קשר 'is-a' צמוד. לדוגמה, a GoldenRetriever הוא Dog, אשר הוא Animal.

ירושה בהקשר שאינו React

בואו נסתכל על דוגמה פשוטה של מחלקת JavaScript כדי לבסס את המושג:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Calls the parent constructor
    this.breed = breed;
  }

  speak() { // Overrides the parent method
    console.log(`${this.name} barks.`);
  }

  fetch() {
    console.log(`${this.name} is fetching the ball!`);
  }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // Output: "Buddy barks."
myDog.fetch(); // Output: "Buddy is fetching the ball!"

במודל זה, המחלקה Dog מקבלת אוטומטית את המאפיין name ואת השיטה speak מה-Animal. היא יכולה גם להוסיף שיטות משלה (fetch) ולדרוס שיטות קיימות. זה יוצר היררכיה נוקשה.

מדוע ירושה נכשלת ב-React

בעוד שמודל 'is-a' זה עובד עבור מבני נתונים מסוימים, הוא יוצר בעיות משמעותיות כאשר מיישמים אותו לרכיבי ממשק משתמש ב-React:

בגלל בעיות אלה, צוות React עיצב את הספריה סביב פרדיגמה גמישה וחזקה יותר: קומפוזיציה.

אימוץ הדרך של React: כוחה של קומפוזיציה

קומפוזיציה היא עיקרון עיצובי המעדיף קשר 'has-a' או 'uses-a'. במקום שרכיב יהיה רכיב אחר, יש לו רכיבים אחרים או שהוא משתמש בפונקציונליות שלהם. רכיבים מטופלים כבלוקים לבנייה - כמו לבני LEGO - שניתן לשלב אותם בדרכים שונות כדי ליצור ממשקי משתמש מורכבים מבלי להיות נעולים בהיררכיה נוקשה.

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

טכניקה 1: הכלה עם `props.children`

הצורה הפשוטה ביותר של קומפוזיציה היא הכלה. זה המקום שבו רכיב משמש כקונטיינר גנרי או 'תיבה', והתוכן שלו מועבר מרכיב הורה. ל-React יש prop מיוחד מובנה לכך: props.children.

תאר לעצמך שאתה צריך רכיב `Card` שיכול לעטוף כל תוכן עם גבול וצל עקביים. במקום ליצור וריאציות של `TextCard`, `ImageCard` ו-`ProfileCard` באמצעות ירושה, אתה יוצר רכיב `Card` גנרי אחד.

// Card.js - A generic container component
function Card(props) {
  return (
    <div className="card">
      {props.children}
    </div>
  );
}

// App.js - Using the Card component
function App() {
  return (
    <div>
      <Card>
        <h1>Welcome!</h1>
        <p>This content is inside a Card component.</p>
      </Card>

      <Card>
        <img src="/path/to/image.jpg" alt="An example image" />
        <p>This is an image card.</p>
      </Card>
    </div>
  );
}

כאן, הרכיב Card לא יודע או אכפת לו מה הוא מכיל. הוא פשוט מספק את סגנון העטיפה. התוכן בין תגיות <Card> הפתיחה והסגירה מועבר אוטומטית כ-props.children. זוהי דוגמה יפה של ניתוק ויכולת שימוש חוזר.

טכניקה 2: התמחות עם Props

לפעמים, רכיב צריך מספר 'חורים' שימולאו על ידי רכיבים אחרים. בעוד שאתה יכול להשתמש ב-`props.children`, דרך מפורשת ומובנית יותר היא להעביר רכיבים כ-props רגילים. דפוס זה נקרא לעתים קרובות התמחות.

שקול רכיב `Modal`. למודאל יש בדרך כלל מקטע כותרת, מקטע תוכן ומקטע פעולות (עם כפתורים כמו "אשר" או "ביטול"). אנו יכולים לעצב את ה-`Modal` שלנו כדי לקבל את המקטעים האלה כ-props.

// Modal.js - A more specialized container
function Modal(props) {
  return (
    <div className="modal-backdrop">
      <div className="modal-content">
        <div className="modal-header">{props.title}</div>
        <div className="modal-body">{props.body}</div>
        <div className="modal-footer">{props.actions}</div>
      </div>
    </div>
  );
}

// App.js - Using the Modal with specific components
function App() {
  const confirmationTitle = <h2>Confirm Action</h2>;
  const confirmationBody = <p>Are you sure you want to proceed with this action?</p>;
  const confirmationActions = (
    <div>
      <button>Confirm</button>
      <button>Cancel</button>
    </div>
  );

  return (
    <Modal
      title={confirmationTitle}
      body={confirmationBody}
      actions={confirmationActions}
    />
  );
}

בדוגמה זו, Modal הוא רכיב פריסה שניתן לשימוש חוזר מאוד. אנו מתמחים בו על ידי העברת אלמנטי JSX ספציפיים עבור ה-`title`, `body` וה-`actions` שלו. זה הרבה יותר גמיש מיצירת תתי מחלקות של `ConfirmationModal` ו-`WarningModal`. אנו פשוט מלחינים את ה-`Modal` עם תוכן שונה לפי הצורך.

טכניקה 3: רכיבים מסדר גבוה (HOCs)

לשיתוף לוגיקה שאינה ממשק משתמש, כגון אחזור נתונים, אימות או רישום, מפתחי React פנו בעבר לתבנית הנקראת רכיבים מסדר גבוה (HOCs). בעוד שרובם הוחלפו על ידי Hooks ב-React מודרנית, חיוני להבין אותם מכיוון שהם מייצגים שלב אבולוציוני מרכזי בסיפור הקומפוזיציה של React ועדיין קיימים בבסיסי קוד רבים.

HOC היא פונקציה שלוקחת רכיב כארגומנט ומחזירה רכיב חדש ומשופר.

בואו ניצור HOC בשם `withLogger` שרושם את ה-props של רכיב בכל פעם שהוא מתעדכן. זה שימושי לאיתור באגים.

// withLogger.js - The HOC
import React, { useEffect } from 'react';

function withLogger(WrappedComponent) {
  // It returns a new component...
  return function EnhancedComponent(props) {
    useEffect(() => {
      console.log('Component updated with new props:', props);
    }, [props]);

    // ... that renders the original component with the original props.
    return <WrappedComponent {...props} />;
  };
}

// MyComponent.js - A component to be enhanced
function MyComponent({ name, age }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>You are {age} years old.</p>
    </div>
  );
}

// Exporting the enhanced component
export default withLogger(MyComponent);

הפונקציה `withLogger` עוטפת את `MyComponent`, ומעניקה לה יכולות רישום חדשות מבלי לשנות את הקוד הפנימי של `MyComponent`. אנחנו יכולים ליישם את אותו HOC על כל רכיב אחר כדי לתת לו את אותה תכונת רישום.

אתגרים עם HOCs:

טכניקה 4: Render Props

תבנית Render Prop הופיעה כפתרון לחלק מהמגרעות של HOCs. היא מציעה דרך מפורשת יותר לשיתוף לוגיקה.

רכיב עם render prop לוקח פונקציה כ-prop (בדרך כלל נקראת `render`) וקורא לפונקציה זו כדי לקבוע מה להציג, ולהעביר לה כל מצב או לוגיקה כארגומנטים.

בואו ניצור רכיב `MouseTracker` שעוקב אחר קואורדינטות X ו-Y של העכבר וגורם להם להיות זמינים לכל רכיב שרוצה להשתמש בהם.

// MouseTracker.js - Component with a render prop
import React, { useState, useEffect } from 'react';

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (event) => {
    setPosition({ x: event.clientX, y: event.clientY });
  };

  useEffect(() => {
    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);

  // Call the render function with the state
  return render(position);
}

// App.js - Using the MouseTracker
function App() {
  return (
    <div>
      <h1>Move your mouse around!</h1>
      <MouseTracker
        render={mousePosition => (
          <p>The current mouse position is ({mousePosition.x}, {mousePosition.y})</p>
        )}
      />
    </div>
  );
}

כאן, `MouseTracker` מכיל את כל הלוגיקה למעקב אחר תנועת העכבר. זה לא מציג שום דבר בפני עצמו. במקום זאת, הוא מפקיד את לוגיקת העיבוד ל-prop `render` שלו. זה מפורש יותר מ-HOCs מכיוון שאתה יכול לראות בדיוק מאיפה הנתונים של `mousePosition` מגיעים ממש בתוך ה-JSX.

ניתן להשתמש גם ב-prop `children` כפונקציה, שהיא וריאציה נפוצה ואלגנטית של תבנית זו:

// Using children as a function
<MouseTracker>
  {mousePosition => (
    <p>The current mouse position is ({mousePosition.x}, {mousePosition.y})</p>
  )}
</MouseTracker>

טכניקה 5: Hooks (הגישה המודרנית והמועדפת)

Hooks, שהוצגו ב-React 16.8, חוללו מהפכה באופן שבו אנו כותבים רכיבי React. הם מאפשרים לך להשתמש במצב ותכונות React אחרות ברכיבים פונקציונליים. והכי חשוב, Hooks מותאמים אישית מספקים את הפתרון האלגנטי והישיר ביותר לשיתוף לוגיקה סטטית בין רכיבים.

Hooks פותרים את הבעיות של HOCs ו-Render Props בצורה הרבה יותר נקייה. בואו נעצב מחדש את דוגמת ה-`MouseTracker` שלנו לתוך hook מותאם אישית בשם `useMousePosition`.

// hooks/useMousePosition.js - A custom Hook
import { useState, useEffect } from 'react';

export function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({ x: event.clientX, y: event.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // Empty dependency array means this effect runs only once

  return position;
}

// DisplayMousePosition.js - A component using the Hook
import { useMousePosition } from './hooks/useMousePosition';

function DisplayMousePosition() {
  const position = useMousePosition(); // Just call the hook!

  return (
    <p>
      The mouse position is ({position.x}, {position.y})
    </p>
  );
}

// Another component, maybe an interactive element
import { useMousePosition } from './hooks/useMousePosition';

function InteractiveBox() {
  const { x, y } = useMousePosition();

  const style = {
    position: 'absolute',
    top: y - 25, // Center the box on the cursor
    left: x - 25,
    width: '50px',
    height: '50px',
    backgroundColor: 'lightblue',
  };

  return <div style={style} />;
}

זהו שיפור עצום. אין 'wrapper hell', אין התנגשויות שמות prop, ואין פונקציות render prop מורכבות. הלוגיקה מנותקת לחלוטין לפונקציה הניתנת לשימוש חוזר (`useMousePosition`), וכל רכיב יכול 'להתחבר' ללוגיקה הסטטית הזו עם שורת קוד יחידה וברורה. Hooks מותאמים אישית הם הביטוי האולטימטיבי של קומפוזיציה ב-React המודרנית, ומאפשרים לך לבנות ספרייה משלך של בלוקי לוגיקה שניתנים לשימוש חוזר.

השוואה מהירה: קומפוזיציה מול ירושה ב-React

כדי לסכם את ההבדלים העיקריים בהקשר של React, הנה השוואה ישירה:

היבט ירושה (Anti-Pattern ב-React) קומפוזיציה (מועדפת ב-React)
יחסים 'is-a' relationship. רכיב מיוחד הוא גרסה של רכיב בסיסי. 'has-a' או 'uses-a' relationship. לרכיב מורכב יש רכיבים קטנים יותר או שהוא משתמש בלוגיקה משותפת.
צימוד גבוה. רכיבי ילד מצומדים היטב ליישום של ההורה שלהם. נמוך. רכיבים עצמאיים וניתן לעשות בהם שימוש חוזר בהקשרים שונים מבלי לשנות אותם.
גמישות נמוכה. היררכיות נוקשות מבוססות מחלקות מקשות על שיתוף לוגיקה בין עצי רכיבים שונים. גבוהה. ניתן לשלב ולעשות שימוש חוזר בלוגיקה ובממשק המשתמש בדרכים אינספור, כמו לבני בניין.
שימוש חוזר בקוד מוגבל להיררכיה המוגדרת מראש. אתה מקבל את כל ה"גורילה" כשאתה רוצה רק את ה"בננה". מצוין. רכיבים ו-Hooks קטנים וממוקדים יכולים לשמש בכל היישום.
React Idiom לא מומלץ על ידי צוות React הרשמי. הגישה המומלצת והאידיומטית לבניית יישומי React.

מסקנה: תחשוב בקומפוזיציה

הוויכוח בין קומפוזיציה וירושה הוא נושא יסוד בעיצוב תוכנה. בעוד שלירשה יש את מקומה ב-OOP קלאסי, האופי הדינמי, מבוסס הרכיבים של פיתוח ממשק משתמש הופך אותה ללא מתאימה ל-React. הספרייה תוכננה באופן יסודי לאמץ קומפוזיציה.

על ידי העדפת קומפוזיציה, אתה מרוויח:

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

ארכיטקטורת רכיבי React: מדוע הקומפוזיציה מנצחת את הירושה | MLOG