• בלוג
  • עמוד 160
  • שימוש ב Render Props כדי להפוך קומפוננטה לגנרית יותר

שימוש ב Render Props כדי להפוך קומפוננטה לגנרית יותר

20/07/2020

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

1. הקומפוננטה המקורית

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

function FilteredList({ items }) {
  const [filter, setFilter] = React.useState("");
  const visibleItems = items.filter(item => item.text.includes(filter));
  return (
    <div>
      <label>
        Filter
        <input
          type="text"
          value={filter}
          onChange={e => setFilter(e.target.value)}
        />
      </label>
      <ul>
        {visibleItems.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  );
}

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

2. פונקציית פילטר

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

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

function FilteredList({ items, filterFn }) {
  const [filter, setFilter] = React.useState("");
  const visibleItems = filterFn(filter, items);
  return (
    <div>
      <label>
        Filter
        <input
          type="text"
          value={filter}
          onChange={e => setFilter(e.target.value)}
        />
      </label>
      <ul>
        {visibleItems.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  );
}

FilteredList.defaultProps = {
  filterFn: (filter, items) => items.filter(item => item.text.includes(filter))
};

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

export default function App() {
  const items = [
    { id: 0, text: "one" },
    { id: 1, text: "two" },
    { id: 2, text: "three" }
  ];
  return (
    <div className="App">
      <FilteredList
        items={items}
        filterFn={(filter, items) =>
          items.filter(i => i.text.toLowerCase().includes(filter.toLowerCase()))
        }
      />
    </div>
  );
}

3. רנדר פרופס: קבלת שיטת רינדור פריט מבחוץ

עכשיו שאנחנו מרגישים בנוח עם העברת פונקציה בתור prop, אפשר לדבר על תבנית מאוד פופולרית בריאקט בשם Render Props. הרעיון של התבנית הוא שכשיש לנו חלק בקומפוננטה שצריך לרנדר UI, ואנחנו רוצים לכתוב אותו בצורה גנרית, אנחנו יכולים להעביר פונקציה שתקבל כפרמטרים את המאפיינים של אותו "דבר" שצריך לרנדר, ותחזיר את ה Virtual DOM Element המתאים.

במקרה של הרשימה בהחלט אפשר לדמיין שמישהו ירצה לרנדר כל פריט ברשימה בצורה אחרת - אולי עם class מסוים או אולי להציג גם את ה ID שלו. בואו נאפשר את זה באמצעות Render Props. ערך ברירת המחדל שוב יהיה פשוט li, אבל עכשיו אפשר יהיה לעשות את ההתאמה מבחוץ:

function FilteredList({ items, filterFn, renderItem }) {
  const [filter, setFilter] = React.useState("");
  const visibleItems = filterFn(filter, items);
  return (
    <div>
      <label>
        Filter
        <input
          type="text"
          value={filter}
          onChange={e => setFilter(e.target.value)}
        />
      </label>
      <ul>{visibleItems.map(item => renderItem(item))}</ul>
    </div>
  );
}

FilteredList.defaultProps = {
  filterFn: (filter, items) => items.filter(item => item.text.includes(filter)),
  renderItem(item) {
    return <li key={item.id}>{item.text}</li>;
  }
};

ושוב אם אנחנו רוצים להעביר ערך אחר בתור ה render function קל לעשות את זה מבחוץ:

export default function App() {
  const items = [
    { id: 0, text: "one" },
    { id: 1, text: "two" },
    { id: 2, text: "three" }
  ];
  return (
    <div className="App">
      <FilteredList
        items={items}
        filterFn={(filter, items) =>
          items.filter(i => i.text.toLowerCase().includes(filter.toLowerCase()))
        }
        renderItem={item => (
          <li key={item.id}>
            {item.id} - {item.text}
          </li>
        )}
      />
    </div>
  );
}

4. רנדר פרופס: קבלת רכיב הפילטור מבחוץ

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

הנה הקוד:

function FilteredList({ items, filterFn, renderItem, filterInput }) {
  const [filter, setFilter] = React.useState("");
  const visibleItems = filterFn(filter, items);
  return (
    <div>
      {filterInput(filter, setFilter)}
      <ul>{visibleItems.map(item => renderItem(item))}</ul>
    </div>
  );
}

FilteredList.defaultProps = {
  filterFn: (filter, items) => items.filter(item => item.text.includes(filter)),
  renderItem(item) {
    return <li key={item.id}>{item.text}</li>;
  },
  filterInput(filter, setFilter) {
    return (
      <label>
        Filter
        <input
          type="text"
          value={filter}
          onChange={e => setFilter(e.target.value)}
        />
      </label>
    );
  }
};

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

אם אתם רוצים לשחק קצת עם הקוד העליתי אותו לסנדבוקס בקישור הזה: https://codesandbox.io/s/beautiful-bush-336fj?file=/src/App.js:51-772