Event Handling

Mungkin kami sudah berkali-kali mengatakan bahwa perubahan data terjadi karena ada input atau interaksi pengguna. Pertanyaannya, bagaimana cara menangkap interaksi pengguna di React, contohnya seperti mendeteksi adanya aksi click pada sebuah tombol? Oke mari kita bahas.

Jika Anda sudah familier dengan DOM manipulation dan event handling, seharusnya memahami cara kerja event handling di React tidaklah susah. Sejatinya event handling di React mirip dengan apa yang kita lakukan pada DOM element. Contohnya, secara standar kita dapat menetapkan event pada element dengan cara seperti ini.

<button onclick="increaseValue()">
  Increase Value
</button>

Berbeda dengan React, untuk menetapkan event, Anda dapat melakukannya dengan cara seperti berikut.

<button onClick={increaseValue}>
  Increase Value
</button>

Dalam pemberian nama event, alih-alih menggunakan lowercase, React selalu menggunakan camelCase. Jadi penulisan event onclick di React adalah onClick.

Ketika menggunakan React, Anda juga tidak perlu lagi menggunakan perintah addEventListener() untuk menetapkan event setelah element ditampilkan pada DOM. Cukup sediakan event handler pada React element secara langsung melalui properti.


Props Drilling Event Handler

Data atau state sebaiknya berada di parent component dan hanya boleh diubah oleh komponen itu sendiri sehingga fungsi handler--yang notabene akan mengubah state--haruslah berada di parent component.

Ketika Anda memecah belah komponen menjadi komponen yang lebih kecil, kemungkinan Anda akan menemukkan kasus di mana component yang menerima input pengguna--contohnya tombol--dibuat secara terpisah dari parent component. Ketika ini terjadi, Anda perlu memberikan event handler melalui props secara drilling. Hal ini sangat umum dilakukan karena React menganut konsep unidirectional data flow.

Perhatikan bagan berikut.


State count hidup di dalam component CounterApp yang merupakan parent component dari seluruh komponen yang ditampilkan. Karena CounterApp pemilik data, maka ia-lah yang berhak memperbarui datanya melalui fungsi onIncreaseEventHandler yang dimilikinya.

Bagan di atas menunjukkan bahwa kita memecah UI berdasarkan tugas. Tugas pertama yaitu menampilkan data count didelegasikan pada component CounterScreen sehingga komponen tersebut diberikan data count oleh parent (CounterApp) melalui props.

Selanjutnya, tugas keduanya yaitu menerima input--untuk menambah nilai count--yang didelegasikan kepada component IncreaseButton. Di sinilah praktik props drilling event handler dilakukan. IncreaseButton tidak berhak untuk mengubah data count, itulah sebabnya ia diberikan fungsi handler milik parent melalui props. Nantinya fungsi handler digunakan ketika IncreaseButton menerima sinyal input (click) dari pengguna.

Data yang disimpan di dalam state bersifat reaktif. Jika datanya berubah, ia akan me-render ulang komponen yang ditampilkan sehingga data yang tampak akan selalu up-to-date.

Sampai di sini, semoga Anda sudah memiliki gambaran bagaimana menerapkan event handling pada React. Supaya pemahaman Anda lebih dalam, mari kita praktikkan teori yang sudah Anda dapatkan sejauh ini di modul selanjutnya.


Latihan Component State dan Event Handling

Di latihan kali ini, kita akan merealisasikan aplikasi counter dengan menuliskan kode secara langsung menggunakan React. Sebelum mulai, ketahuilah tujuan dari latihan yang akan kita lakukan di bawah ini.

  • Mengetahui cara menyimpan data di dalam state.
  • Mengetahui cara mengubah data di dalam state.
  • Memanfaatkan data di dalam state untuk menampilkan UI.
  • Mengetahui cara event handling pada component.

Kita akan membangun aplikasi counter dengan improvisasi agar lebih menantang. Kami sebut aplikasi ini dengan nama “FizzBuzz Counter”. Kurang lebih fungsi dan tampilannya tampak seperti ini.


FizzBuzz Counter ini sama seperti counter pada umumnya, tetapi ada sedikit perbedaan yaitu:

  • bila angka termasuk kelipatan 5, aplikasi akan menampilkan Fizz,
  • bila angka termasuk kelipatan 7, aplikasi akan menampilkan Buzz, dan
  • bila angka termasuk kelipatan 5 dan 7, aplikasi akan menampilkan FizzBuzz.

Aplikasi di atas juga terdapat tombol increase untuk menambahkan nilai dan reset untuk mengatur ulang nilai.

Yuk kita mulai latihannya!

  1. Buka tautan dicoding-react-starter atau Anda juga boleh membuat proyek react baru secara lokal menggunakan CRA.

  2. Selanjutnya, buat komponen placeholder CounterApp menggunakan class component dan render komponennya pada DOM.

    import React from 'react';
    import { createRoot } from 'react-dom/client';
     
    class CounterApp extends React.Component {
      render() {
        return (
          <div>
            <p>TODO: selesaikan component-nya!</p>
          </div>
        );
      }
    }
     
    const root = createRoot(document.getElementById('root'));
    root.render(<CounterApp />);
    

    Simpan perubahan dan kini pada browser tampak seperti ini.


  3. Lakukan inisialisasi state di dalam component CounterApp, lalu simpan nilai count di dalam this.state dengan menuliskan kode pada constructor seperti berikut.

    import React from 'react';
    import { createRoot } from 'react-dom/client';
     
    class CounterApp extends React.Component {
      constructor(props) {
        super(props);
     
        // inisialisasi nilai count di dalam state
        this.state = {
          count: 0
        };
      }
     
      render() {
        return (
          <div>
            <p>TODO: selesaikan component-nya!</p>
          </div>
        );
      }
    }
     
    const root = createRoot(document.getElementById('root'));
    root.render(<CounterApp />);
    

  4. Kemudian, buat dua method (event handler), yaitu onIncreaseEventHandler dan onResetEventHandler yang berfungsi untuk menaikkan dan mereset nilai dari count.

    import React from 'react';
    import { createRoot } from 'react-dom/client';
     
    class CounterApp extends React.Component {
      constructor(props) {
        super(props);
     
        // inisialisasi nilai count di dalam state
        this.state = {
          count: 0
        };
      }
     
      onIncreaseEventHandler() {
        this.setState((previousState) => {
          return {
            count: previousState.count + 1
          };
        });
      }
      onResetEventHandler() {
        this.setState(() => {
          return {
            count: 0
          };
        });
      }
     
      render() {
        return (
          <div>
            <p>TODO: selesaikan component-nya!</p>
          </div>
        );
      }
    }
     
    const root = createRoot(document.getElementById('root'));
    root.render(<CounterApp />);
    

    Masih ingatkah Anda dengan aturan pengelolaan data? Karena CounterApp menampung data, komponen ini yang berhak mengubah datanya. Oleh karena itu, kita perlu mendefinisikan fungsi event handler di sini.

  5. Selanjutnya, kita akan membuat component untuk menampilkan data count. Masih di berkas yang sama, silakan buat component baru dengan nama CounterDisplay.

    import React from 'react';
    import { createRoot } from 'react-dom/client';
     
    function CounterDisplay({ count }) {
     return <p>{count}</p>;
    }
     
    // kode CounterApp disembunyikan ...
    

    Pembuatan component ini cukup dengan functional component karena kita tidak memanfaatkan state. Data count yang ditampilkan pada component ini akan diberikan melalui props oleh CounterApp.

  6. Sekarang, buat component baru bernama IncreaseButton yang digunakan sebagai tombol untuk meningkatkan nilai count setiap kali diklik.

    import React from 'react';
    import { createRoot } from 'react-dom/client';
     
    function CounterDisplay({ count }) {
      return <p>{count}</p>;
    }
     
    function IncreaseButton({ increase }) {
      return (
        <div>
          <button onClick={increase}>+ increase</button>
        </div>
      );
    }
     
    // kode CounterApp disembunyikan ...
    

    Komponen IncreaseButton menerima satu props dengan nama increase. Props increase merupakan event handler yang nantinya akan dipanggil setiap terjadi event onClick.

  7. Buat lagi component baru--serupa dengan IncreaseButton--bernama ResetButton untuk mereset nilai dari count.

    import React from 'react';
    import { createRoot } from 'react-dom/client';
     
    function CounterDisplay({ count }) {
      return <p>{count}</p>;
    }
     
    function IncreaseButton({ increase }) {
      return (
        <div>
          <button onClick={increase}>+ increase</button>
        </div>
      );
    }
     
    function ResetButton({ reset }) {
      return (
        <div>
          <button onClick={reset}>- reset</button>
        </div>
      );
    }
     
    // kode CounterApp disembunyikan ...
    

  8. Setelah seluruh komponen terbuat, kini saatnya untuk menggunakan component CounterDisplay, IncreaseButton, dan ResetButton di dalam CounterApp. Ubahlah kode di dalam fungsi render pada component CounterAppmenjadi seperti ini.

    class CounterApp extends React.Component {
     // kode lain disembunyikan
     
      render() {
        return (
          <div>
            <IncreaseButton increase={this.onIncreaseEventHandler} />
            <CounterDisplay count={this.state.count} />
            <ResetButton reset={this.onResetEventHandler} />
          </div>
        );
      }
    }
    

    Simpan perubahan dan sekarang browser akan menampilkan UI seperti ini.


    Sayangnya aplikasi belum bisa berfungsi karena ketika Anda coba menekan tombol increase atau reset akan menghasilkan eror.


  9. Eror tersebut disebabkan oleh nilai this pada fungsi onIncreaseEventHandler bukan bernilai instance dari komponen CounterApp melainkan window. Kok bisa? Kami coba jelaskan sedikit ya.

    Di JavaScript, nilai this yang digunakan pada fungsi belum tentu bernilai instance atau objek (termasuk component) dari pemilik fungsi tersebut. Melainkan, this bernilai instance/objek di mana fungsi tersebut dipanggil.

    Oke, saat ini memang fungsi handler yang dibuat berada di dalam component CounterApp, tetapi fungsi dipanggil oleh IncreaseButton. Sehingga, nilai this ketika fungsi tersebut dipanggil bukan CounterApp melainkan window (IncreaseButton tidak memiliki this-nya sendiri karena functional component, sehingga nilai this adalah window).

    Untuk menyelesaikan eror ini, kita perlu mengikat konteks dari nilai this pada fungsi handler agar tetap CounterApp. Caranya, kita lakukan binding nilai this pada fungsi event handler tersebut. Silakan tambahkan kode berikut pada constructor component CounterApp.

    class CounterApp extends React.Component {
      constructor(props) {
        super(props);
     
        // inisialisasi nilai count di dalam state
        this.state = {
          count: 0
        };
     
        // binding event handler
        this.onIncreaseEventHandler = this.onIncreaseEventHandler.bind(this);
        this.onResetEventHandler = this.onResetEventHandler.bind(this);
      }
     
      // kode lain disembunyikan ...
    }
    

    Melalui binding fungsi event handler dengan nilai this pada constructor, kini nilai this di dalam fungsi event handler akan selalu bernilai CounterApp terlepas di mana pun fungsi tersebut dipanggil.

    Simpan perubahan kode di atas dan klik kembali tombol yang tampak pada browser. Seharusnya aplikasi sudah berfungsi dengan baik saat ini.


  10. Terakhir, kita tinggal menerapkan aturan FizzBuzz ketika menampilkan datanya. Kita perlu menulis logika FizzBuzz di dalam component CounterDisplay karena component itulah yang bertugas menampilkan data.

    Fokus pada component CounterDisplay, silakan ubah kode di dalam component menjadi seperti ini.

    function CounterDisplay({ count }) {
      if (count === 0) {
        return <p>{count}</p>;
      }
     
      if (count % 5 === 0 && count % 7 === 0) {
        return <p>FizzBuzz</p>;
      }
     
      if (count % 5 === 0) {
        return <p>Fizz</p>;
      }
     
      if (count % 7 === 0) {
        return <p>Buzz</p>;
      }
     
      return <p>{count}</p>;
    }
    

    Catatan: Kode di atas menunjukkan conditional rendering di React dapat dilakukan dengan sintaksis if dan else standar karena sejatinya React hanyalah JavaScript.

    Simpan perubahan kode pada component CounterDisplay dan kini aplikasi counter sudah berjalan sesuai harapan.


Latihan Studi Kasus: Menambahkan Fitur Hapus Kontak

Sejauh ini Anda sudah berhasil membuat class component dan memanfaatkan state dalam menyimpan data. Sekarang saatnya kita lanjutkan kembali proyek aplikasi daftar kontak yang sudah dibuat pada modul sebelumnya.

Setelah mempelajari state dan event handling, Anda mampu membuat data di dalam komponen dapat lebih hidup. Sekarang saatnya kita menambahkan fitur baru guna mengimplementasikan apa yang sudah Anda pelajari. Fitur yang akan dibangun adalah hapus kontak. Dengan hadirnya fitur ini, diharapkan aplikasi dapat menghapus kontak yang ditampilkan.


Sudah siap untuk memulai latihan? Yuk, kita lanjut.

  1. Silakan buka kembali proyek contacts-app pada VSCode.
  2. Kemudian buat berkas JavaScript baru bernama DeleteButton.js pada folder src -> components.
  3. Di dalamnya, tuliskan kode untuk membuat component DeleteButton.

    import React from 'react';
     
    function DeleteButton({ id, onDelete }) {
      return <button className='contact-item__delete' onClick={() => onDelete(id)}>X</button>
    }
     
    export default DeleteButton;
    

    Component DeleteButton menerima dua properti, yakni id dan onDelete. Properti id digunakan sebagai referensi id kontak yang hendak dihapus, sedangkan onDelete merupakan handler yang akan dikirim secara drilling sebagai aksi dalam menghapus kontak.

    Catatan: Mengapa terdapat “on”? Kata “on” pada penamaan delete sebenarnya digunakan untuk menghindari JavaScript reserved word. Kita tidak bisa membuat variabel/properti bernama delete.

  4. Selanjutnya, gunakan component DeleteButton di dalam component ContactItem. Jangan lupa tambahkan dan berikan juga props id dan onDelete karena component DeleteButton membutuhkannya.

    import React from 'react';
    import ContactItemBody from './ContactItemBody';
    import ContactItemImage from './ContactItemImage';
    import DeleteButton from './DeleteButton';
     
    function ContactItem({ imageUrl, name, tag, id, onDelete }) {
     return (
       <div className="contact-item">
         <ContactItemImage imageUrl={imageUrl} />
         <ContactItemBody name={name} tag={tag} />
         <DeleteButton id={id} onDelete={onDelete} />
       </div>
     );
    }
     
    export default ContactItem;
    

  5. Simpan seluruh perubahan kode dan jalankan proyek dengan perintah berikut.

    npm run start
    

    Seharusnya sekarang tombol hapus sudah tampak tetapi belum bisa digunakan.


    Ini wajar karena kita belum menetapkan nilai event handler yang sebenarnya.

  6. Karena sekarang component ContactItem membutuhkan properti id dan onDelete, kita perlu menyediakan nilai dari kedua properti tersebut pada component yang menggunakannya, yaitu ContactList. Silakan buka berkas ContactList.jsdan ubah kodenya menjadi seperti ini.

    import React from 'react';
    import ContactItem from './ContactItem';
     
    function ContactList({ contacts, onDelete }) {
      return (
        <div className="contact-list">
          {
            contacts.map((contact) => (
              <ContactItem 
              key={contact.id}
              id={contact.id}
              onDelete={onDelete}
              {...contact} />
            ))
          }
        </div>
      );
    }
     
    export default ContactList;
    

    Untuk properti onDelete, kita masih melakukan drilling karena nilai handler sebenarnya ada pada component ContactApp selaku pemilik data contacts.

  7. Selanjutnya, kita ubah pembuatan component ContactApp menjadi class component. Jangan lupa saat ini kita perlu menyimpan data contacts di dalam this.state agar perubahan datanya memicu render UI. Kemudian buat juga method onDeleteEventHandler untuk menangani event ketika tombol hapus diklik.

    Silakan buka berkas ContactApp.js dan ubah kode di dalamnya menjadi seperti ini.

    import React from 'react';
    import ContactList from './ContactList';
    import { getData } from '../utils/data';
     
    class ContactApp extends React.Component {
     constructor(props) {
       super(props);
       this.state = {
         contacts: getData(),
       }
     
       this.onDeleteHandler = this.onDeleteHandler.bind(this);
     }
     
     onDeleteHandler(id) {
       const contacts = this.state.contacts.filter(contact => contact.id !== id);
       this.setState({ contacts });
     }
     
     render() {
       return (
         <div className="contact-app">
           <h1>Daftar Kontak</h1>
           <ContactList contacts={this.state.contacts} onDelete={this.onDeleteHandler} />
         </div>
       );
     }
    }
     
    export default ContactApp;
    

    Simpan seluruh perubahan dan coba kembali aplikasi pada Browser. Seharusnya tombol hapus sudah berfungsi dengan baik.


React Hooks?

“Mengapa tidak menggunakan hooks?” -- Materi ini kami sajikan untuk menjawab pertanyaan tersebut.

React--begitu juga dengan cara mempelajarinya--berada di tahap yang “aneh” saat ini. Hal ini dikarenakan hadirnya fitur “Hooks” sejak React versi 16.8. Hooks mengubah konsep komponen dalam mengelola state. Sebelum hadirnya hooks, state hanya dapat dimanfaatkan oleh class component, tetapi sekarang dapat juga dimanfaatkan oleh functional component. Lantas apa dampaknya bagi Anda yang hendak belajar React saat ini?

Oke, kami rasa kasus ini mirip dengan penambahan arrow function yang dilakukan oleh JavaScript. Ketika arrow function hadir, tidak berarti seluruh pengetahuan yang kita miliki terkait regular function sudah tidak relevan, bukan? Alih-alih menggantikan, arrow function sebenarnya hanya cara lain dalam mendefinisikan fungsi di JavaScript. React hooks pun demikian.

Seluruh konsep dan cara mempelajari React akan tetap sama, walau dengan hooks Anda bisa memanfaatkan component state dengan cara lain. Hal ini ditegaskan oleh React melalui dokumentasi hooks yang mengatakan “Hooks tidak mengubah pengetahuan dari konsep React.”. Hooks hanya menawarkan API yang dapat mempercepat penggunaan konsep-konsep React (salah satunya state) yang sebenarnya sudah Anda ketahui.

React team juga membuat pernyataan yang jelas untuk tidak mengubah implementasi class component yang ada dengan menuliskan ulang menggunakan hooks. Namun, Anda bisa menerapkan hooks pada kode baru yang hendak dituliskan, itu pun jika Anda mau.

Namun, ingat ya! Jika Anda ingin menggunakan hooks, bukan berarti Anda boleh melewati proses memahami dasar dari state itu sendiri. Sialnya, Anda harus mempelajari keduanya. Karena jika Anda bekerja pada industri yang sudah lama menggunakan React dan kodenya belum menerapkan hooks, kami khawatir Anda kebingungan.

Tidak perlu sedih bila kelas ini tidak mengajarkan atau mengimplementasikan hooks karena kami ingin di awal pembelajaran Anda fokus untuk mengenal konsep dasar dari React itu sendiri. Hooks akan kami ajarkan di kelas terpisah setelah Anda memahami bagaimana implementasi React sebelum hadirnya hooks.


Sebelumnya : Stateful Component Selanjutnya : Controlled Component