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.
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!
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:
- Identifikasi berbagai state komponen visual Anda
- Tentukan apa yang menyebabkan perubahan state tersebut
- Representasikan state tersebut dalam memori dengan menggunakan
useState
- Hapus variabel state yang tidak esensial
- 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
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.
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.
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.
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
danisSubmit
tidak mungkin bernilaitrue
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 sebuahstatus
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
danisTyping
tidak dapat bernilaitrue
pada saat yang bersamaan. Dengan menjadikannya variabel state yang terpisah, Anda berisiko membuat keduanya tidak sinkron dan mengakibatkan bug. Untungnya, Anda dapat menghapusisEmpty
dan sebagai gantinya memeriksaanswer.length === 0
. - Bisakah Anda mendapatkan informasi yang sama dari kebalikan dari variabel state yang lain?
isError
tidak diperlukan karena Anda bisa memeriksaerror !== 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
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:
- Identifikasi semua visual state-nya.
- Tentukan pemicu dari pengguna dan komputer untuk perubahan state.
- Memodelkan state dengan
useState
. - Hapus state yang tidak esensial untuk menghindari bug dan kondisi paradoks.
- 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> ); }