Membuat Form dengan React Hook Form

Di ekosistem ReactJS ada beberapa library yang bisa kita pake untuk membantu kita membuat form. Ada React Hook Form Formik, React Final Form, dan lain-lain. Dalam artikel kali ini, saya akan bahas tentang React Hook Form (RHF) & integrasinya dengan Material UI.

RHF menyediakan 6 hook yang bisa kita pake dalam pembuatan form, khususnya di bagian logic-nya karena untuk UI kita bebas pake apa yang kita mau — boleh pake MUI, Chakra, form input default dan lain-lain. Dari 6 hook itu kemungkinan besar kita cukup pake satu aja, useForm() – kecuali untuk kasus-kasus spesial di mana kita harus bikin form yang cukup rumit & nggak bisa dihandel pake useForm() aja.

Starter projectnya bisa di-fork di sini: rhf-starter.

Basic Form

Di starter project itu kita punya form yang bentuknya begini:

Buka fail MyForm.jsx. Tambahkan kode untuk useForm. Hook ini nilai baliknya adalah objek yang berisi fungsi-fungsi utilitas untuk manajemen form. Yang paling dasar adalah fungsi register() & handleSubmit().

import { useForm } from 'react-hook-form';

function MyForm(props) {
  const { handleSubmit, register } = useForm();

  const doSubmit = (data) => {
    console.log('SUBMITTING', data);
  };

  return (
    <Paper>
      <h1>Formulir</h1>
      <form onSubmit={handleSubmit(doSubmit)}>
        <Stack spacing={2}>
          <Paper>
            <h2>Bagian 1</h2>
            <Stack spacing={2}>
              <TextField
                placeholder="Nama"
                label="Nama"
                {...register('name')}
              />
              <TextField
                placeholder="Email"
                label="Email"
                {...register('email')}
              />
              <TextField
                type="password"
                label="Password"
                {...register('password')}
              />
              <TextField
                type="password"
                label="Konfirmasi Password"
                {...register('passwordConfirmation')}
              />
            </Stack>
          </Paper>
        </Stack>


//... kode selanjutnya

Fungsi handleSubmit() adalah fungsi untuk di-assign ke properti onSubmit-nya <form>. Fungsi ini nggak langsung mengirim (submit) data tapi akan  menerima objek data yang berisi pasangan key-value data from & mengeksekusi callback kalo data form lolos validasi. Callback-nya, dalam kode di atas, adalah fungsi doSubmit() – fungsi inilah yang akan mengirim data.

Dalam kode di atas, kita nggak pake validasi, jadi pas tombol Kirim diklik, handleSubmit() langsung menjalankan doSubmit().

Fungsi register() nilai baliknya adalah objek yang berisi onChange(), onBlur(), ref & name. Objek ini langsung kita spread jadi properti masing-masing <TextField>.  Argumen fungsi ini adalah nama field  di data form untuk input yang bersangkutan. Kode di bawah artinya, value dari <TextField> disimpan sebagai field name. Kita nggak perlu lagi menulis kode untuk menghandel perubahan nilai <TextField> secara manual.

<TextField {...register('name')} />

Argumen pertama dari register(), nggak hanya bisa dikasih nama field yang sederhana, tapi juga bisa dipakai untuk menentukan struktur di dalam data form  di mana value-nya nanti disimpan.

register('primary.name')
register('persons[0].email')

Kode di atas artinya nanti kita punya data form seperti ini:

{
  primary:{
    name: '...'
  },
  persons:[
   {
     email: '....'
   }
  ]
}

Validasi

Parameter kedua dari register() adalah objek yang berisi skema validasi. Atribut validasinya ada macam-macam, yang paling sederhana adalah required , artinya input nggak boleh kosong. Ada juga validasi minLength & maxLength untuk jumlah karakter.

<TextField
  placeholder="Nama"
  label="Nama"
  {...register('name', {
      required: true,
      minLength: 5
    })
  }
/>
<TextField
  placeholder="Email"
  label="Email"
  {...register('email', {
      required: true
    })
  }
/>

Dalam kode di atas,  field name harus diisi & panjangnya minimal 5 karakter. Field email, harus diisi, nggak ada panjang minimal.

Kalo name nggak diisi, pas kita klik tombol Kirim, fungsi doSubmit() nggak dijalanin karena ada error/nggak lolos validasi. Otomatis input untuk name masuk fokus.  Begitu juga kalo email nggak lolos validasi.

Selain skema yang formatnya sederhana, kita bisa buat yang lebih komplit, dengan error message. Salah satu cara untuk mengecek error yang ada di form adalah lewat fungsi yang kita kirim sebagai argumen kedua untuk handleSubmit().  Di sini kita buat fungsi namanya onError. Terus ubah skema validasi untuk name seperti di bawah.

const onError = (error) => {
  console.log(error);
};

return (
  <Paper>
  <h1>Formulir</h1>
  {/* pake onError sebagai parameter kedua */}
  <form onSubmit={handleSubmit(doSubmit, onError)}>
    <Stack spacing={2}>
      <Paper>
         <h2>Bagian 1</h2>
         <Stack spacing={2}>
            <TextField
               placeholder="Nama"
               label="Nama"
               {...register('name', {
                  required: 'Nama harus diisi',
                  minLength: {
                    value: 5,
                    message: 'Minimal 5 karakter'
                  }
                })}
             />

Kalo name nggak diisi terus kita klik tombol Kirim, objek yang diterima onError() bisa kita liat di konsol.

Selain pake callback seperti di atas, untuk mengecek error kita bisa pake objek formState yang kita dapat dari useForm().

const { handleSubmit, register, formState } = useForm();

const doSubmit = (data) => {
  console.log('SUBMITTING', data);
};

// nggak pake onError lagi

return (
  <Paper>
    <h1>Formulir</h1>
    <form onSubmit={handleSubmit(doSubmit)}>
      <Stack spacing={2}>
        <Paper>
          <h2>Bagian 1</h2>
          <Stack spacing={2}>
            <TextField
              helperText={formState.errors.name?.message}
              placeholder="Nama"
              label="Nama"
              {...register('name', {
                required: 'Nama harus diisi',
                minLength: {
                  value: 5,
                  message: 'Minimal 5 karakter'
                }
              })}
            />

Dalam kode di atas, kalo name nggak lolos validasi di bawah inputnya akan muncul helper-text yang berisi pesan error yang relevan. Kalo nggak diisi, pesannya sesuai dengan pesan untuk validasi required. Kalo diisi kurang dari 5 karakter, pesannya juga sesuai yang kita tentukan untuk validasi minLength.

Untuk field yang isinya harus sesuai dengan pola (pattern) tertentu, email misalnya,  kita bisa pake Regex.

<TextField
  helperText={formState.errors.email?.message}
  placeholder="Email"
  label="Email"
  {...register('email', {
    required: 'Email harus diisi',
    pattern: {
      value: /([\w.\-_]+)?\w+@[\w-_]+(.\w+){1,}/i,
      message: 'Email tidak valid'
    }
  })}
/>

Masih banyak teknik validasi yang bisa kita pake. Yang di atas itu termasuk yang paling sederhana. Teknik lainnya bisa dibaca sendiri di sini.

Default Values

Kalo kita buat form untuk mengedit data, pastinya kita harus isi form dengan data yang mau diedit, bukan hanya form kosong. Caranya kita kirim objek yang isinya field defaultValues sebagai argumen useForm().

const { handleSubmit, register, formState } = useForm({
    defaultValues: {
      name: 'pak boss',
      email: 'pak@boss.com'
    }
  });

Form Reset

Untuk me-reset form kita pake reset() dari useForm(). Tinggal panggil tanpa parameter kalo kita ingin form di-reset ke default-value, atau objek kosong kalo kita ingin form benar-benar kosong.

const onReset = () => {
  reset(); // kembalikan ke default-values
  // reset({}); <-- bersihkan form
};

return (
  <Paper>
    <h1>Formulir</h1>
    <form onSubmit={handleSubmit(doSubmit)}>
      {/*... kode yang lain */}

      <Box className="footer">
        <Button onClick={onReset} color="warning" variant="outlined">
          Reset
        </Button>
        <Button type="submit" variant="contained">
          Kirim
        </Button>
      </Box>
    </form>
  </Paper>
);

Input Dinamis

Bikin input dinamis relatif gampang. Tinggal bikin state yang nantinya menentukan berapa banyak input yang dibuat, atributnya apa aja. Setelah itu untuk setiap input kita panggil register() untuk bikin field di formState.  Pas form-nya direset, state-nya juga harus ikut direset.

import React, { useState } from 'react';
import { Box, Button, Paper, Stack, TextField } from '@mui/material';
import { useForm } from 'react-hook-form';
import { Add, Delete } from '@mui/icons-material';

function MyForm(props) {
  const { handleSubmit, register, formState, reset } = useForm({
    defaultValues: {
      name: 'pak boss',
      email: 'pak@boss.com'
    }
  });

  const [additionalSections, setAdditionalSections] = useState([]);

  const addSection = () => {
    setAdditionalSections((curr) => {
      const id = Date.now();
      return [
        ...curr,
        {
          id,
          label: `Tambahan `,
          name: `additional-${id}`
        }
      ];
    });
  };

  const deleteSection = (id) => {
    setAdditionalSections((curr) => {
      return curr.filter((obj) => obj.id !== id);
    });
  };

  const doSubmit = (data) => {
    console.log('SUBMITTING', data);
  };

  const onReset = () => {
    // reset input tambahannya juga
    setAdditionalSections([]);
    reset();
  };

  return (
    <Paper>
      <h1>Formulir</h1>
      <form onSubmit={handleSubmit(doSubmit)}>
        <Stack spacing={2}>
          {/* baris sebelumnya */}

          {additionalSections.map((obj) => {
            return (
              <Paper key={obj.id}>
                <TextField
                  label={obj.label}
                  {...register(`additional.${obj.name}`, {
                    required: 'Harus diisi'
                  })}
                />

                <Button
                  onClick={() => {
                    deleteSection(obj.id);
                  }}
                  startIcon={<Delete />}
                >
                  Hapus
                </Button>
              </Paper>
            );
          })}
        </Stack>

        <Button startIcon={<Add />} onClick={addSection}>
          Tambah Input
        </Button>

        {/*baris selanjutnya*/}
      </form>
    </Paper>
  );
}

export default MyForm;

Dibanding library lain seperti Formik, RHF ini rasanya lebih mudah dipahami. Untuk form yang nggak terlalu rumit, kita cukup pake API seperti register(), formState, handleSubmit. Untuk keperluan form yang lebih rumit banyak API lain yang bisa kita pake – yang juga relatif mudah dipahami. Selain itu, RHF juga bisa dipake di React Native.

Jadi itu sedikit tentang React-Hook-Form. Kalo mau tau lebih banyak tentang useForm() & hook yang lain bisa baca-baca dokumentasinya di sini.