שינוי קומפוננטות מבחוץ

27/09/2025

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

export default function Counter() {
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>Count = {count}</p>
            <button onClick={() => setCount(c => c + 1)}>+1</button>
        </div>
    );
}

אני רוצה להוסיף לו כפתור איפוס, אבל בלי לשנות את הקובץ המקורי של Counter. אפילו אם אני מוכן לשנות את הקריאות ל Counter מבחוץ כלומר לעטוף אותו באיזה CounterWithReset אין לי איך לעטוף את הקומפוננטה ולשנות את ה Virtual DOM שהיא מחזירה. זה פשוט לא בכללים.

בואו נראה שתי דוגמאות לספריות שכן מאפשרות מנגנון כזה ומה הופך אותו לאפשרי.

1. הרחבת Counter ב lit

דוגמה ראשונה היא lit שבונה קומפוננטות באמצעות Web Components. ב lit קומפוננטת Counter נראית כך:

import { LitElement, html } from 'lit';

export class MyCounter extends LitElement {
  static properties = {
    count: { type: Number }
  };

  constructor() {
    super();
    this.count = 0;
  }

  render() {
    return html`
      <button @click=${() => this.count--}>-</button>
      <span>${this.count}</span>
      <button @click=${() => this.count++}>+</button>
    `;
  }
}

customElements.define('my-counter', MyCounter);

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

import { html } from 'lit';
import { MyCounter } from "./my-counter";

// Inject a reset method
MyCounter.prototype.reset = function () {
  this.count = 0;
  this.requestUpdate(); // tell Lit to re-render
};

// Patch render to add a Reset button
const origRender = MyCounter.prototype.render;
MyCounter.prototype.render = function () {
  return html`
    ${origRender.call(this)}
    <button @click=${() => this.reset()}>Reset</button>
  `;
};

export default MyCounter;

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

2. הרחבת Counter ב Stimulus

גישה שנייה להרחבה היא של ריילס וספריית ה JavaScript שלה, סטימולוס. ה Counter בסטימולוס מורכב מקובץ JavaScript ומקובץ html.erb. זו התבנית:

<div 
  data-controller="counter" 
  data-counter-count-value="0"
>
  <button data-action="counter#decrement">-</button>
  <span data-counter-target="output">0</span>
  <button data-action="counter#increment">+</button>
</div>

וזה קובץ ה JavaScript:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["output"]
  static values = { count: Number }

  connect() {
    this.update()
  }

  increment() {
    this.countValue++
    this.update()
  }

  decrement() {
    this.countValue--
    this.update()
  }

  update() {
    this.outputTarget.textContent = this.countValue
  }
}

רוצים להוסיף איפוס? אין בעיה אפשר לעדכן את ה Controller מבחוץ ולהוסיף פונקציה:

import CounterController from "./counter_controller"

CounterController.prototype.reset = function () {
  this.countValue = 0;
  this.update()
}

את התבנית אפשר לשנות או לרנדר אותה מקובץ טמפלייט אחר.

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