• בלוג
  • בואו נכתוב קומפוננטה שניתנת לעריכה ב React

בואו נכתוב קומפוננטה שניתנת לעריכה ב React

07/01/2020

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

כך קרה לי עם אלמנטים שניתנים לעריכה - למרות שחיפשתי ומצאתי מספר ספריות ב npm עבור Editable React Components, בסוף לכל אחת היו את הבעיות שלה ומימוש לבד של הקוד נתן לי את השליטה הכי מדויקת. וזה אפילו לא היה כזה מסובך.

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

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

function EditableLabel_View(props) {
  const { text, startEdit } = props;
  return (<p onClick={startEdit}>{text}</p>);
}

רק קומפוננטת התצוגה יודעת מתי צריך לעבור למצב עריכה ולכן אני מעדיף להעביר את הפונקציה startEdit בתור property ולתת לקוד בתוך הקומפוננטה לבחור מתי להשתמש בזה.

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

function EditableLabel_Edit(props) {
  const { text, setText, done } = props;

  function doneIfEnter(e) {
    if (e.keyCode === 13) {
      done();
    }
  }

  return (
    <input
      type="text"
      value={text}
      onChange={(e) => setText(e.target.value)}
      onBlur={done}
      onKeyDown={doneIfEnter}
      />
  );
}

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

function EditableLabel(props) {
  const [text, setText] = useState(props.text);
  const [edit, setEdit] = useState(false);

  if (edit) {
    return <EditableLabel_Edit text={text} setText={setText} done={() => setEdit(false)} />
  } else {
    return <EditableLabel_View text={text} startEdit={() => setEdit(true)} />
  }
}

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

class App extends React.Component {
  render() {
    return (
      <div>
        <EditableLabel text="Hello world" />
      </div>
    );
  }
}

ואפשר לראות את הקסם כולו בפעולה בקודפן הבא:

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

function makeEditableComponent(View, Edit) {
  return function EditableLabel(props) {
    const [data, setData] = useState(props.defaultValue);
    const [edit, setEdit] = useState(false);

    if (edit) {
      return <Edit data={data} setData={setData} done={() => setEdit(false)} />
    } else {
      return <View data={data} startEdit={() => setEdit(true)} />
    }
  }
}

היתרון הוא שעכשיו קל מאוד לייצר את EditableLabel או כל קומפוננטה דומה פשוט באמצעות הפעלת הפונקציה:

const EditableLabel = makeEditableComponent(EditableLabel_View, EditableLabel_Edit);

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