• בלוג
  • פורטל לתוך קומפוננטה אחרת

פורטל לתוך קומפוננטה אחרת

23/10/2022

לא מזמן נתקלתי בטריק החמוד הזה בריאקט שרציתי לשתף, לא בשביל שתשתמשו בו בפרודקשן אלא יותר כ Proof Of Concept, כדי להבין עוד דבר או שניים על פורטלים ו ref.

המשחק הוא פשוט - יש לי קומפוננטה שמנהלת סטייט, נניח רשימת משימות, ואני רוצה לכתוב במקום אחר (לא בתוך תת העץ שלה) כמה משימות פתוחות יש במערכת. ביום רגיל היינו לוקחים את הסטייט, מעלים אותו לקומפוננטה הקרובה ביותר בעץ שמשותפת לשתי הקומפוננטות ומעבירים לכל קומפוננטה את החלק שהיא צריכה. אבל היום לא יום רגיל.

1. נקודת התחלה

אני מתחיל עם הקומפוננטה הבאה:

function TodoList() {
  const [items, setItems] = useState([
    { id: 1, text: "one", done: false },
    { id: 2, text: "two", done: false },
    { id: 3, text: "three", done: false },
    { id: 4, text: "four", done: false }
  ]);

  function toggle(item) {
    setItems((oldItems) =>
      oldItems.map((it) => (it.id === item.id ? { ...it, done: !it.done } : it))
    );
  }

  return (
    <>
      <p>Open Tasks: {items.filter((it) => !it.done).length}</p>
      <ul>
        {items.map((i) => (
          <li key={i.id}>
            <label>
              <input
                type="checkbox"
                checked={i.done}
                onChange={(e) => toggle(i)}
              />
              {i.text}
            </label>
          </li>
        ))}
      </ul>
    </>
  );
}

אפשר לראות אותה בקודסנדבוקס כאן: https://codesandbox.io/s/summer-monad-1ezijz?file=/src/App.js:42-837

הקומפוננטה מציגה רשימת פריטים ולחיצה על כל פריט משנה את שדה done שלו. היא גם מציגה טקסט שאומר כמה משימות פתוחות יש.

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

2. הפיתרון: פורטל

פיתרון לא שגרתי לבעיה יהיה לבנות פורטל. פורטל זו דרך של קומפוננטה לרנדר חלק מעצמה בתוך "מקום" אחר, כשבדרך כלל המקום האחר הוא אלמנט חדש שהיא יוצרת ב body בשביל להציג דיאלוג מודאלי, אבל אותו מקום אחר יכול להיות גם ref שמגיע מקומפוננטה אחרת.

הטריק היחיד כאן הוא להשתמש ב Callback Ref כדי להעביר את הערך המעודכן של הפורטל לקומפוננטה אחרי כל render.

בתוך קומפוננטת המשימות אני מחליף את פיסקת הטקסט ב:

{portal &&
  createPortal(
    <p>Open Tasks: {items.filter((it) => !it.done).length}</p>,
    portal
  )}

כש portal זה property שעובר מבחוץ, ובקומפוננטה החיצונית אני מייצר את האלמנט portal באופן הבא:

export default function App() {
  const [portal, setPortal] = useState(null);

  return (
    <div className="App">
      <div ref={(node) => setPortal(node)} />
      <TodoList portal={portal} />
    </div>
  );
}

הדוגמה המלאה בקודסנדבוקס בקישור: https://codesandbox.io/s/zealous-architecture-nrskck