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.

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!

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({ // 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
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 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.
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.


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
andisSubmitting
can’t both betrue
. 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 astatus
that must be one of three values:'typing'
,'submitting'
, or'success'
. - Is the same information available in another state variable already? Another paradox:
isEmpty
andisTyping
can’t betrue
at the same time. By making them separate state variables, you risk them going out of sync and causing bugs. Fortunately, you can removeisEmpty
and instead checkanswer.length === 0
. - Can you get the same information from the inverse of another state variable?
isError
is not needed because you can checkerror !== 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
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:
- Identify all its visual states.
- Determine the human and computer triggers for state changes.
- Model the state with
useState
. - Remove non-essential state to avoid bugs and paradoxes.
- 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> ); }