Berdamai Dengan Multiple Props Pada Reusable Component

Berdamai Dengan Multiple Props Pada Reusable Component

Zakiego

Zakiego

@zakiego

Latar Belakang

Masalah ini bermula karena saya sangat banyak mengerjakan project dengan banyak (sekali) input. Sudah tidak lagi bisa dibilang, dari input yang sederhana sampai bertingkat tujuh! Kompleks, satu input terkait dengan input yang lain.

Namun, input-input tersebut memiliki satu pola yang sama, yaitu pasti memiliki element <label> dan <input>, akhirnya agar lebih rapi dan bisa digunakan berulang tanpa harus menulis dari awal, maka dibuatlah satu component yang reusable.

Biasanya component ini diletakkan di /components/UI/Form/Input.tsx

Komponen yang sifatnya agnostik, dalam artian tidak terikat dengan hal apa pun, bisa digunakan di mana saja, diletakkan di UI.

Sebut saja nama component tersebut adalah <FormInput/>. Kita kesampingkan dulu masalah styling, maka kodenya akan seperti ini:

interface FormInputProps {
  label: string
  name: string
}

const FormInput: React.FC<FormInputProps> = (props) => {
  return (
    <div>
      <label htmlFor={props.name}>{props.label}</label>
      <div>
        <input id={props.name} name={props.name} />
      </div>
    </div>
  )
}

const Page = () => {
  return (
    <div>
      <FormInput label="Name" name="name" />
    </div>
  )
}

<FormInput/> adalah sebuah komponen yang akan menerima dan label dan name untuk sebagai props.

Satu masalah terselesaikan. Kita bisa menggunakan <FormInput/> di mana saja, tanpa harus menulis <label> dan <input> berulang-ulang.

Masalah Baru

Tapi kemudian muncul masalah baru, bagaimana jika ingin meng-customize bagian <label> dan input <input>?

Misalnya kita ingin labelnya berwarna biru, dan inputnya berwarna hijau?

Mari kita coba menambahkan className sebagai props:

interface FormInputProps {
  label: string
  name: string
  className?: string
}

const FormInput: React.FC<FormInputProps> = (props) => {
  return (
    <div>
      <label htmlFor={props.name} className={props.className}>
        {props.label}
      </label>
      <div>
        <input id={props.name} name={props.name} className={props.className} />
      </div>
    </div>
  )
}

const Page = () => {
  return (
    <div>
      <FormInput
        label="Name"
        name="name"
        className="text-blue-500 text-green-500" // PERHATIKAN INI
      />
    </div>
  )
}

Lihat kode di atas pada bagian className miliki komponen FormInput, bagaimana jadinya satu className yaitu text-blue-500 text-green-500 diletakkan pada className yang sama, kemudian digunakan untuk <label>dan <input>?

Yang terjadi justru adalah text-green-500 yang akan diambil, kemudian mengubah warna label dan input menjadi sama-sama hijau.

Bagaimana jika pisahkan, antara className label dan input? Sehingga propsnya akan terpisah.

interface FormInputProps {
  label: string
  name: string
  classNameLabel?: string
  classNameInput?: string
}

const FormInput: React.FC<FormInputProps> = (props) => {
  return (
    <div>
      <label htmlFor={props.name} className={props.classNameLabel}>
        {props.label}
      </label>
      <div>
        <input
          id={props.name}
          name={props.name}
          className={props.classNameInput}
        />
      </div>
    </div>
  )
}

const Page = () => {
  return (
    <div>
      <FormInput
        label="Name"
        name="name"
        classNameLabel="text-blue-500"
        classNameInput="text-green-500"
      />
    </div>
  )
}

Bagus!

Sekarang label akan menjadi biru, dan input menjadi hijau. Component sudah menjadi lebih customable.

Masalah Lain

Satu element <FormInput> sudah beres. Tapi kemudian, saya ingin kembali membuat <FormInput> dengan type password?

Rasa-rasanya akan menjadi kian kompleks, saya perlu menambahkan satu-persatu props yang diperlukan. Akhirnya component tidak se-elastis yang kita inginkan.

interface FormInputProps {
  label: string
  name: string
  classNameLabel?: string
  classNameInput?: string
  type?: string
}

const FormInput: React.FC<FormInputProps> = (props) => {
  return (
    <div>
      <label htmlFor={props.name} className={props.classNameLabel}>
        {props.label}
      </label>
      <div>
        <input
          id={props.name}
          name={props.name}
          className={props.classNameInput}
          type={props.type}
        />
      </div>
    </div>
  )
}

const Page = () => {
  return (
    <div>
      <FormInput
        label="Name"
        name="name"
        classNameInput="text-blue-500"
        classNameLabel="text-green-500"
      />
      <FormInput
        label="Password"
        name="password"
        classNameInput="text-blue-500"
        classNameLabel="text-green-500"
        type="password"
      />
    </div>
  )
}

Kode akan menjadi seperti di atas, props terlihat tidak natural dan kurang enak dibaca.

Jalan Keluar

Kita bisa membuat props lebih natural dan fleksibel, tanpa harus menambahkannya satu-persatu. Bagaimana caranya?

Metode ini saya temukan ketika sedang melakukan riset untuk bermigrasi dari formik ke react-hook-form. Saat melakukan riset mengenai cara membuat reusable component di react-hook-form, saya menemukan kode dari repository ini input-control/index.tsx.

Kita cukup membuat props berupa labelProps dan inputProps yang nantinya akan diisi dengan object kemudian dimasukkan dengan spread syntax (...).

interface FormInputProps {
  label: string
  name: string
  labelProps?: React.DetailedHTMLProps<
    React.LabelHTMLAttributes<HTMLLabelElement>,
    HTMLLabelElement
  >
  inputProps?: React.DetailedHTMLProps<
    React.InputHTMLAttributes<HTMLInputElement>,
    HTMLInputElement
  >
}

const FormInput: React.FC<FormInputProps> = (props) => {
  return (
    <div>
      <label htmlFor={props.name} {...props.labelProps}>
        {props.label}
      </label>
      <div>
        <input id={props.name} name={props.name} {...props.inputProps} />
      </div>
    </div>
  )
}

const Page = () => {
  return (
    <div>
      <FormInput
        label="Name"
        name="name"
        inputProps={{
          className: 'text-blue-500',
        }}
        labelProps={{
          className: 'text-green-500',
        }}
      />
      <FormInput
        label="Password"
        name="password"
        inputProps={{
          className: 'text-blue-500',
          type: 'password',
          onChange: (e) => {
            console.log('Password changed:', e.target.value)
          },
          autoComplete: 'password',
        }}
        labelProps={{
          className: 'text-green-500',
        }}
      />
    </div>
  )
}

Selesai!

Sekarang hanya ada dua props yang perlu kita isi dan bisa dicustom sekehandak hati. Kita bisa memasukkan props yang biasa digunakan di label dan input tanpa takut overlap satu sama lain. Bahkan di contoh di atas, kita bisa memasukkan props onChange dan type.

Penutup

Mengenai type pada interface, bisa kita bicarakan di lain waktu. Akhir kata, ketika membuat reusable component, kita perlu membuatnya dengan ketat namun tetap fleksibel dan terhindar dari error. Agar kita tidak menulis kode yang sama berulang, kode mudah dibaca, namun sekaligus tidak ada error yang mengganggu tidur.