Merespon Masukan dengan State

React menyediakan cara deklaratif untuk memanipulasi UI. Alih-alih memanipulasi bagian-bagian UI secara langsung, Anda deskripsikan berbagai state yang berbeda yang dapat terjadi di dalam komponen Anda, dan mengalihkan antara berbagai kemungkinan state tersebut sebagai respons terhadap masukan pengguna. Cara ini mirip dengan bagaimana desainer berpikir tentang UI.

Anda akan mempelajari

  • Bagaimana pemrograman UI deklaratif berbeda dari pemrograman UI imperatif
  • Bagaimana cara melakukan enumerasi atas berbagai state visual yang berbeda dapat terjadi pada komponen Anda
  • Bagaimana cara memicu perubahan antara berbagai state visual yang berbeda melalui kode

Membandingkan UI deklaratif dengan imperatif

Ketika Anda mendesain interaksi UI, Anda mungkin berpikir tentang bagaimana UI berubah dalam menanggapi tindakan pengguna. Pertimbangkan sebuah formulir yang memungkinkan pengguna mengirimkan sebuah jawaban:

  • Ketika Anda mengetikkan sesuatu kedalam formulir, maka tombol “Kirim” menjadi aktif
  • Ketika Anda menekan tombol “Kirim”, baik formulir maupun tombol “Kirim” tersebut menjadi nonaktif dan sebuah spinner muncul.
  • Apabila permintaan jaringan berhasil, formulir disembunyikan dan pesan “Terima Kasih” muncul.
  • Apabila permintaan jaringan gagal, sebuah pesan kesalahan muncul, dan formulir menjadi aktif kembali.

Pada pemrograman imperatif, yang disebutkan di atas berkaitan langsung dengan bagaimana Anda mengimplementasikan interaksi tersebut. Anda harus menulis intruksi yang spesifik untuk memanipulasi UI tergantung apa yang sedang terjadi. Cara lain untuk memikirkan hal ini adalah: bayangkan menumpang disebelah seseorang di dalam mobil dan memberitahu mereka kemana harus pergi disetiap belokan.

Di dalam mobil yang dikemudikan oleh orang yang tampak khawatir, merepresentasikan JavaScript, seorang penumpang memerintahkan pengemudi untuk melakukan serangkaian navigasi belokan demi belokan yang rumit.

Ilustrasi oleh Rachel Lee Nabors

Dia tidak tahu kemana Anda ingin pergi, dia hanya mengikuti perintah yang Anda berikan (dan apabila Anda memberikan arah yang salah, Anda akan sampai ditempat yang salah juga). Hal ini disebut imperatif karena Anda harus ” memberi perintah” pada setiap elemen, dari pemintal hingga tombol, memberi tahu komputer bagaimana cara untuk memperbarui UI tersebut.

Pada contoh pemrograman antarmuka imperatif, formulir dibangun tanpa menggunakan React, hanya menggunakan DOM peramban:

async function handleFormSubmit(e) {
  e.preventDefault();
  disable(textarea);
  disable(button);
  show(loadingMessage);
  hide(errorMessage);
  try {
    await submitForm(textarea.value);
    show(successMessage);
    hide(form);
  } catch (err) {
    show(errorMessage);
    errorMessage.textContent = err.message;
  } finally {
    hide(loadingMessage);
    enable(textarea);
    enable(button);
  }
}

function handleTextareaChange() {
  if (textarea.value.length === 0) {
    disable(button);
  } else {
    enable(button);
  }
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

function enable(el) {
  el.disabled = false;
}

function disable(el) {
  el.disabled = true;
}

function submitForm(answer) {
  // Anggap saja sedang menghubungi jaringan.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (answer.toLowerCase() === 'istanbul') {
        resolve();
      } else {
        reject(new Error('Tebakan yang bagus, tapi salah. Coba lagi!'));
      }
    }, 1500);
  });
}

let form = document.getElementById('form');
let textarea = document.getElementById('textarea');
let button = document.getElementById('button');
let loadingMessage = document.getElementById('loading');
let errorMessage = document.getElementById('error');
let successMessage = document.getElementById('success');
form.onsubmit = handleFormSubmit;
textarea.oninput = handleTextareaChange;

Memanipulasi UI secara imperatif bekerja dengan cukup baik untuk contoh-contoh tertentu, tetapi menjadi jauh lebih sulit untuk dikelola dalam sistem yang lebih kompleks. Bayangkan jika Anda memperbarui halaman yang penuh dengan berbagai macam formulir seperti formulir di atas. Menambahkan elemen UI baru atau interaksi baru akan memerlukan pemeriksaan yang hati-hati terhadap semua kode yang ada untuk memastikan bahwa Anda tidak membuat bug (misalnya, lupa menampilkan atau menyembunyikan sesuatu).

React dibangun untuk mengatasi masalah ini.

Pada React, Anda tidak perlu memanipulasi antarmuka secara langsung, maksudnya Anda tidak perlu mengaktifkan, menonaktifkan, menampilkan, atau menyembunyikan suatu component secara langsung. Melainkan, Anda dapat mendeklarasikan apa yang ingin Anda tampilkan, dan React akan memperbarui antarmuka tersebut. Bayangkan Anda menyewa taksi dan memberitahu pengemudinya kemana Anda akan pergi, daripada memberitahukan di mana ia harus berbelok. Itu adalah tugas pengemudi untuk mencari tahu bagaimana mengantar Anda ke tujuan, bahkan dia bisa menemukan jalan pintas yang tidak Anda tahu!

Di dalam mobil yang dikemudikan oleh React, seorang penumpang meminta untuk diantarkan ke tempat tertentu pada peta. React akan mencari cara menuju ke tempat tersebut.

Ilustrasi oleh Rachel Lee Nabors

Berpikir tentang UI secara deklaratif

Anda telah melihat bagaimana cara mengimplementasikan sebuah formulir secara imperatif di atas. Untuk lebih memahami cara berpikir dalam React, Anda akan mempelajari cara mengimplementasikan ulang UI berikut ini dalam React:

  1. Identifikasi berbagai state komponen visual Anda
  2. Tentukan apa yang menyebabkan perubahan state tersebut
  3. Representasikan state tersebut dalam memori dengan menggunakan useState
  4. Hapus variabel state yang tidak esensial
  5. Hubungkan event handler untuk mengatur state tersebut

Langkah 1: Identifikasi berbagai state komponen visual Anda

Dalam ilmu komputer, Anda mungkin pernah mendengar tentang state machine yang merupakan salah satu dari beberapa “state”. Jika Anda bekerja dengan seorang desainer, Anda mungkin pernah melihat model visual untuk “visual state” yang berbeda. React terletak pada persimpangan antara desain dan ilmu komputer, sehingga kedua ide ini menjadi sumber inspirasi.

Pertama, Anda perlu memvisualisasikan seluruh “state” UI yang mungkin akan dilihat oleh pengguna:

  • Kosong: Formulir memiliki tombol “Kirim” yang dinonaktifkan.
  • Mengetik: Formulir memiliki tombol “Kirim” yang diaktifkan.
  • Mengirimkan: Formulir sepenuhnya dinonaktifkan. Spinner ditampilkan.
  • Sukses: Pesan “Terima kasih” ditampilkan, menggantikan formulir.
  • Kesalahan: Sama seperti state Mengetik, namun dengan tambahan pesan kesalahan.

Sama seperti seorang desainer, Anda pasti ingin “model visual” atau membuat “tiruan” untuk berbagai state sebelum menerapkan logika. Sebagai contoh, berikut ini adalah mock hanya untuk bagian visual dari formulir. Mock ini dikontrol oleh sebuah prop yang disebut status dengan nilai default 'empty':

export default function Form({
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>Benar sekali!</h1>
  }
  return (
    <>
      <h2>Kuis kota</h2>
      <p>
        Di kota manakah terdapat papan reklame yang mengubah udara menjadi air yang dapat diminum?
      </p>
      <form>
        <textarea />
        <br />
        <button>
          Kirim
        </button>
      </form>
    </>
  )
}

Anda dapat menamai prop tersebut dengan nama apa pun yang Anda inginkan, penamaannya tidaklah penting. Cobalah mengubah status = 'kosong' menjadi status = 'sukses' untuk melihat pesan sukses muncul. Mock memungkinkan Anda melakukan iterasi dengan cepat pada UI sebelum Anda menyambungkan logika apa pun. Berikut ini adalah prototipe yang lebih matang dari komponen yang sama, yang masih ” dikontrol” oleh prop status:

export default function Form({
  // Cobalah 'submitting', 'error', 'success':
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>Benar sekali!</h1>
  }
  return (
    <>
      <h2>Kuis kota</h2>
      <p>
        Di kota manakah terdapat papan reklame yang mengubah udara menjadi air yang dapat diminum?
      </p>
      <form>
        <textarea disabled={
          status === 'submitting'
        } />
        <br />
        <button disabled={
          status === 'empty' ||
          status === 'submitting'
        }>
          Submit
        </button>
        {status === 'error' &&
          <p className="Error">
            Tebakan yang bagus, tapi salah. Coba lagi!
          </p>
        }
      </form>
      </>
  );
}

Pendalaman

Menampilkan banyak state visual sekaligus

Jika suatu komponen memiliki banyak state visual, mungkin akan lebih mudah untuk menampilkan semuanya pada satu halaman:

import Form from './Form.js';

let statuses = [
  'empty',
  'typing',
  'submitting',
  'success',
  'error',
];

export default function App() {
  return (
    <>
      {statuses.map(status => (
        <section key={status}>
          <h4>Formulir ({status}):</h4>
          <Form status={status} />
        </section>
      ))}
    </>
  );
}

Halaman seperti ini sering disebut “living styleguides” atau “storybooks”.

Langkah 2: Tentukan apa yang menyebabkan perubahan state tersebut

Anda dapat memicu pembaruan state sebagai respons terhadap dua jenis masukan:

  • Masukan manusia, seperti mengklik tombol, mengetik di kolom, navigasi tautan.
  • Masukan komputer, seperti respon jaringan yang diterima, batas waktu selesai, pemuatan gambar.
Sebuah jari.
Human inputs
Satu dan nol.
Computer inputs

Ilustrasi oleh Rachel Lee Nabors

Pada kedua kasus tersebut, Anda harus mengatur variabel state untuk mengganti UI. Untuk form yang Anda kembangkan, Anda perlu mengubah state sebagai respons terhadap berbagai masukan yang berbeda:

  • Mengubah input teks (manusia) akan mengubahnya dari state Kosong ke state Mengetik atau sebaliknya, tergantung apakah kotak teks kosong atau tidak.
  • Mengklik tombol Kirim (manusia) akan mengalihkannya ke state Mengirimkan.
  • Respons jaringan yang berhasil (komputer) akan mengalihkannya ke state Sukses.
  • Respon jaringan gagal (komputer) akan mengalihkannya ke state Kesalahan dengan pesan kesalahan yang sesuai.

Catatan

Perhatikan bahwa masukan dari manusia sering kali membutuhkan event handler!

Untuk membantu memvisualisasikan alur ini, cobalah gambar setiap state di atas kertas sebagai lingkaran berlabel, dan setiap perubahan di antara dua state sebagai tanda panah. Anda dapat membuat kerangka alur dengan cara ini dan mencegah bug jauh sebelum implementasi.

Diagram alur bergerak dari kiri ke kanan dengan 5 simpul. Simpul pertama yang berlabel 'kosong' memiliki satu edge berlabel 'mulai mengetik' yang terhubung ke simpul berlabel 'mengetik'. Simpul tersebut memiliki satu edge berlabel 'tekan kirim' yang terhubung ke simpul berlabel 'kirim', yang memiliki dua edge. Edge di kiri diberi label 'kesalahan jaringan' yang terhubung ke simpul berlabel 'kesalahan'. Edge di kanan berlabel 'keberhasilan jaringan' yang terhubung ke simpul berlabel 'sukses'.
Diagram alur bergerak dari kiri ke kanan dengan 5 simpul. Simpul pertama yang berlabel 'kosong' memiliki satu edge berlabel 'mulai mengetik' yang terhubung ke simpul berlabel 'mengetik'. Simpul tersebut memiliki satu edge berlabel 'tekan kirim' yang terhubung ke simpul berlabel 'kirim', yang memiliki dua edge. Edge di kiri diberi label 'kesalahan jaringan' yang terhubung ke simpul berlabel 'kesalahan'. Edge di kanan berlabel 'keberhasilan jaringan' yang terhubung ke simpul berlabel 'sukses'.

Berbagai state formulir

Langkah 3: Representasikan state tersebut dalam memori dengan menggunakan useState

Selanjutnya Anda harus merepresentasikan state visual dari komponen Anda di dalam memori dengan useState. Kesederhanaan adalah kuncinya: setiap bagian dari state adalah sebuah “bagian yang bergerak”, dan Anda ingin sesedikit mungkin “bagian yang bergerak” Semakin kompleks maka akan semakin banyak bug!

Mulailah dengan state yang mutlak harus ada di sana. Sebagai contoh, Anda harus menyimpan answer untuk masukan, dan error (jika ada) untuk menyimpan kesalahan sebelumnya:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);

Kemudian, Anda akan membutuhkan variabel state yang mewakili salah satu status visual yang ingin Anda tampilkan. Biasanya ada lebih dari satu cara untuk merepresentasikannya dalam memori, jadi Anda perlu bereksperimen dengannya.

Jika Anda kesulitan untuk menemukan cara terbaik dengan segera, mulailah dengan menambahkan cukup banyak state sehingga Anda yakin bahwa semua keadaan visual yang ada sudah tercakup:

const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);

Ide pertama Anda mungkin bukan yang terbaik, tapi itu bukan masalah, menulis ulang state adalah bagian dari proses!

Langkah 4: Hapus variabel state yang tidak esensial

Anda ingin menghindari duplikasi pada konten state sehingga Anda hanya perlu mencatat state yang penting saja. Luangkan sedikit waktu untuk melakukan penulisan ulang pada struktur state Anda akan membuat komponen Anda lebih mudah dipahami, mengurangi duplikasi, dan menghindari ambiguitas yang tidak diinginkan. Target Anda adalah untuk mencegah kasus di saat state dalam memori tidak merepresentasikan UI valid yang Anda harapkan untuk dilihat oleh pengguna (Sebagai contoh, Anda tidak akan pernah ingin menampilkan pesan kesalahan dan menonaktifkan masukan pada waktu yang sama, atau pengguna tidak akan bisa memperbaiki kesalahan tersebut!)

Berikut adalah beberapa hal yang dapat Anda pertimbangkan tentang variabel state Anda:

  • Apakah keadaan ini menimbulkan kondisi paradoks? Sebagai contoh, isTyping dan isSubmit tidak mungkin bernilai true bersamaan. Kondisi paradoks biasanya menandakan bahwa state tidak dibatasi dengan baik. Ada empat kemungkinan kombinasi dari dua boolean, tetapi hanya tiga yang sesuai dengan state yang sesuai. Untuk menghilangkan state yang “tidak mungkin”, Anda dapat menyatukannya menjadi sebuah status yang harus merupakan salah satu dari tiga nilai: 'typing' (mengetik), 'submitting' (mengirim), atau 'success' (sukses).
  • Apakah informasi yang sama sudah tersedia di variabel status yang lain? Kondisi paradoks lain: isEmpty dan isTyping tidak dapat bernilai true pada saat yang bersamaan. Dengan menjadikannya variabel state yang terpisah, Anda berisiko membuat keduanya tidak sinkron dan mengakibatkan bug. Untungnya, Anda dapat menghapus isEmpty dan sebagai gantinya memeriksa answer.length === 0.
  • Bisakah Anda mendapatkan informasi yang sama dari kebalikan dari variabel state yang lain? isError tidak diperlukan karena Anda bisa memeriksa error !== null sebagai gantinya.

Setelah pembersihan ini, Anda akan memiliki 3 ( berkurang dari 7!) variabel state yang esensial:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', atau 'success'

Anda tahu mereka sangat penting, karena Anda tidak dapat menghapus salah satu dari variabel-variabel tersebut tanpa merusak fungsionalitasnya.

Pendalaman

Menghilangkan state-state “tidak mungkin” dengan reducer

Ketiga variabel ini merupakan representasi yang cukup baik dari state formulir ini. Namun, masih ada beberapa state lanjutan yang tidak sepenuhnya masuk akal. Sebagai contoh, error yang non-null tidak masuk akal ketika status adalah 'success'. Untuk memodelkan state dengan lebih akurat, Anda dapat mengekstraknya ke sebuah reducer. Reducer memungkinkan Anda menyatukan beberapa variabel state ke dalam satu objek dan menggabungkan semua logika yang berhubungan!

Langkah 5: Hubungkan event handler untuk mengatur state tersebut

Terakhir, buatlah event handler yang mengganti state. Berikut ini adalah bentuk akhir, dengan semua event handler terhubung:

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>Benar sekali!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>Kuis kota</h2>
      <p>
        Di kota manakah terdapat papan reklame yang mengubah udara menjadi air yang dapat diminum?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Anggap saja sedang menghubungi jaringan.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Tebakan yang bagus, tapi salah. Coba lagi!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

Meskipun kode ini lebih panjang daripada contoh imperatif semula, kode ini lebih tidak mudah rusak. Mengekspresikan semua interaksi sebagai perubahan state memungkinkan Anda untuk memperkenalkan state visual baru tanpa merusak state yang sudah ada. Hal ini juga memungkinkan Anda untuk mengubah apa yang harus ditampilkan di setiap state tanpa mengubah logika interaksi itu sendiri.

Rekap

  • Pemrograman deklaratif berarti mendeskripsikan UI untuk setiap visual state daripada mengelola UI secara terperinci (imperatif).
  • Saat mengembangkan sebuah komponen:
    1. Identifikasi semua visual state-nya.
    2. Tentukan pemicu dari pengguna dan komputer untuk perubahan state.
    3. Memodelkan state dengan useState.
    4. Hapus state yang tidak esensial untuk menghindari bug dan kondisi paradoks.
    5. Hubungkan event handler untuk menyetel state.

Tantangan 1 dari 3:
Menambah dan menghapus kelas CSS

Buatlah agar mengklik gambar menghapus kelas CSS background--active dari <div> bagian luar, tetapi menambahkan kelas picture--active ke <img>. Mengklik latar belakang lagi akan mengembalikan kelas CSS semula.

Secara visual, Anda seharusnya dapat memperkirakan bahwa dengan mengeklik gambar, Anda akan menghilangkan latar belakang ungu dan menyorot tepian gambar. Mengklik di luar gambar akan menyorot latar belakang, tetapi menghilangkan sorotan tepian gambar tersebut.

export default function Picture() {
  return (
    <div className="background background--active">
      <img
        className="picture"
        alt="Rumah Pelangi di Kampung Pelangi, Indonesia"
        src="https://i.imgur.com/5qwVYb1.jpeg"
      />
    </div>
  );
}