Merespon Masukan dengan State

React menyediakan cara deklaratif untuk memanipulasi UI. Alih-alih memanipulasi bagian-bagian UI secara langsung, Anda dapat membuat berbagai state komponen, dan mengubahnya sebagai respons terhadap masukan pengguna. Cara ini mirip dengan bagaimana desainer memikirkan tentang UI.

Anda akan mempelajari

  • Bagaimana pemrograman UI deklaratif berbeda dari pemrograman UI imperatif
  • Bagaimana cara menjabarkan berbagai state visual yang berbeda pada komponen Anda
  • Bagaimana cara memicu perubahan dari berbagai state

Membandingkan UI deklaratif dengan imperatif

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

  • Anda mengetikan sesuatu kedalam formulir, maka tombol kirim menjadi aktif
  • Anda mengklik tombol “Kirim”, baik formulir maupun tombol “Kirim” tersebut menjadi nonaktif dan spinner muncul.
  • Apabila permintaan jaringan berhasil, formulir disembunyikan dan pesan “Terima Kasih” muncul
  • Apabila permintaan jaringan gagal, 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 mengguna browse DOM:

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 yang terpencil, 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 mengupdate antarmuka tersebut. Pikirkan 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({
  // Try '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 memperbarui 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

You want to avoid duplication in the state content so you’re only tracking what is essential. Spending a little time on refactoring your state structure will make your components easier to understand, reduce duplication, and avoid unintended meanings. Your goal is to prevent the cases where the state in memory doesn’t represent any valid UI that you’d want a user to see. (For example, you never want to show an error message and disable the input at the same time, or the user won’t be able to correct the error!)

Here are some questions you can ask about your state variables:

  • Does this state cause a paradox? For example, isTyping and isSubmitting can’t both be true. A paradox usually means that the state is not constrained enough. There are four possible combinations of two booleans, but only three correspond to valid states. To remove the “impossible” state, you can combine these into a status that must be one of three values: 'typing', 'submitting', or 'success'.
  • Is the same information available in another state variable already? Another paradox: isEmpty and isTyping can’t be true at the same time. By making them separate state variables, you risk them going out of sync and causing bugs. Fortunately, you can remove isEmpty and instead check answer.length === 0.
  • Can you get the same information from the inverse of another state variable? isError is not needed because you can check error !== null instead.

After this clean-up, you’re left with 3 (down from 7!) essential state variables:

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

You know they are essential, because you can’t remove any of them without breaking the functionality.

Pendalaman

Eliminating “impossible” states with a reducer

These three variables are a good enough representation of this form’s state. However, there are still some intermediate states that don’t fully make sense. For example, a non-null error doesn’t make sense when status is 'success'. To model the state more precisely, you can extract it into a reducer. Reducers let you unify multiple state variables into a single object and consolidate all the related logic!

Langkah 5: Connect the event handlers to set state

Lastly, create event handlers that update the state. Below is the final form, with all event handlers wired up:

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) {
  // Pretend it's hitting the network.
  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);
  });
}

Although this code is longer than the original imperative example, it is much less fragile. Expressing all interactions as state changes lets you later introduce new visual states without breaking existing ones. It also lets you change what should be displayed in each state without changing the logic of the interaction itself.

Rekap

  • Declarative programming means describing the UI for each visual state rather than micromanaging the UI (imperative).
  • When developing a component:
    1. Identify all its visual states.
    2. Determine the human and computer triggers for state changes.
    3. Model the state with useState.
    4. Remove non-essential state to avoid bugs and paradoxes.
    5. Connect the event handlers to set state.

Tantangan 1 dari 3:
Add and remove a CSS class

Make it so that clicking on the picture removes the background--active CSS class from the outer <div>, but adds the picture--active class to the <img>. Clicking the background again should restore the original CSS classes.

Visually, you should expect that clicking on the picture removes the purple background and highlights the picture border. Clicking outside the picture highlights the background, but removes the picture border highlight.

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