Seiring berkembangnya aplikasi Anda, penting untuk memperhatikan bagaimana state Anda diatur dan memperhatikan bagaimana data mengalir diantara komponen-komponen yang ada. State yang redundan atau duplikat adalah sumber dari bug dikemudian hari. Dalam babak ini, Anda akan belajar bagaimana menata state dengan baik, bagaimana menjaga logika pembaruan state agar mudah dikelola, dan bagaimana Anda dapat berbagi state dengan komponen yang berjauhan.
Dalam bab ini
- Bagaimana memikirkan perubahan UI sebagai perubahan state
- Bagaimana mengatur state dengan baik
- Bagaimana “menjunjung state” untuk dibagikan ke komponen lain
- Bagaimana menentukan apakah state akan dipertahankan atau dimusnahkan
- Bagaimana menggabungkan logika state yang kompleks dalam sebuah fungsi
- Bagaimana mengirimkan informasi tanpa “prop drilling”
- Bagaimana meningkatkan manajemen state saat aplikasi masih dikembangkan
Merespon masukan dengan state
Dalam React, Anda tidak perlu mengubah kode secara langsung untuk mengubah antar muka (UI). Misalnya, menulis baris perintah “nonaktifkan tombol ketika”, “aktifkan tombol ketika”, “tampilkan pesan sukses ketika”, dll disetiap baris. Melainkan, cukup menggambarkan antar muka yang ingin ditampilkan sebagai states visual dari komponen Anda (“state awal”, “state mengetik”, “state sukses”), dan kemudian memicu perubahan state sebagai respons terhadap masukan pengguna. Sekilas mirip dengan bagaimana desainer merencanakan antar muka.
Berikut contoh formulir kuis yang dibangun menggunakan React. Perhatikan bagaimana ia menggunakan variabel state status
untuk menentukan apakah tombol kirim diaktifkan atau dinonaktifkan, dan apakah pesan sukses ditampilkan sebagai gantinya.
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>Itu Benar!</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 minum? </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 kode ini melakukan *request* ke jaringan. return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Tebakan yang bagus tetapi jawaban salah. Silahkan coba lagi!')); } else { resolve(); } }, 1500); }); }
Siap mempelajari topik ini?
Baca Reacting to Input with State untuk belajar bagaimana mendekati interaksi dengan mindset yang didorong oleh state.
Baca Lebih LanjutMemilih struktur state
Mengatur struktur state dengan baik dapat membuat perbedaan antara komponen yang mudah dimodifikasi dan didebug, dan komponen yang selalu menjadi sumber kesalahan. Perlu dicatat bahwa state tidak boleh mengandung informasi yang tidak perlu atau duplikat. Karena jika ada state yang tidak perlu, mudah untuk lupa memperbarui state tersebut, yang akhirnya memperkenalkan masalah baru!
Misalnya, formulir ini memiliki variabel state fullName
yang redundan:
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); function handleFirstNameChange(e) { setFirstName(e.target.value); setFullName(e.target.value + ' ' + lastName); } function handleLastNameChange(e) { setLastName(e.target.value); setFullName(firstName + ' ' + e.target.value); } return ( <> <h2>Izinkan kami memeriksa Anda</h2> <label> Nama depan:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Nama belakang:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Tiket Anda akan diberikan kepada: <b>{fullName}</b> </p> </> ); }
Anda dapat menghapus dan menyederhanakan kode dengan menghitung fullName
saat komponen di-render:
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = firstName + ' ' + lastName; function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <h2>Izinkan kami memeriksa Anda</h2> <label> Nama depan:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Nama belakang:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Tiket Anda akan diberikan kepada: <b>{fullName}</b> </p> </> ); }
Sekilas seperti perubahan sepele, tetapi umumnya cara ini banyak memperbaiki bug yang ada pada aplikasi React.
Siap mempelajari topik ini?
Baca Choosing the State Structure untuk belajar cara merancang bentuk state untuk menghindari kesalahan (bugs).
Baca Lebih LanjutBerbagi state antar komponen
Terkadang, Anda ingin state dari dua komponen yang berbeda selalu berubah bersama. Untuk melakukannya, hapus state dari keduanya, pindahkan state tersebut ke bagian induk (parent) yang paling berdekatan, dan kemudian teruskan ke kedua komponen melalui props. Hal ini dikenal sebagai “menjunjung state” (lifting state up), dan ini adalah salah satu hal lumrah saat menulis kode React.
Pada contoh ini, dalam satu waktu hanya akan ada satu panel yang aktif. Untuk mencapainya, daripada menyimpan state aktif di setiap panel secara individu, komponen induk menyimpan state dan menentukan props untuk anak-anaknya.
import { useState } from 'react'; export default function Accordion() { const [activeIndex, setActiveIndex] = useState(0); return ( <> <h2>Almaty, Kazakhstan</h2> <Panel title="About" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)} > Dengan populasi sekitar 2 juta orang, Almaty adalah kota terbesar di Kazakhstan. Dari tahun 1929 hingga 1997, kota ini menjadi ibu kota Kazakhstan. </Panel> <Panel title="Etymology" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)} > Nama "Almaty" berasal dari kata <span lang="kk-KZ">алма</span>,dalam bahasa Kazakh yang berarti "apel"dan sering diterjemahkan sebagai "penuh dengan apel". Sebenarnya, wilayah sekitar Almaty dipercaya sebagai asal usul apel, dan <i lang="la">Malus sieversii</i> liar dianggap sebagai kandidat yang mungkin menjadi nenek moyang apel domestik modern. </Panel> </> ); } function Panel({ title, children, isActive, onShow }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={onShow}> Tampilkan </button> )} </section> ); }
Siap mempelajari topik ini?
Baca Berbagi State Antar Komponen untuk mempelajari cara mengangkat state ke atas dan menjaga sinkronisasi antar komponen.
Baca Lebih LanjutMempertahankan dan mengatur ulang state
Saat Anda me-render ulang sebuah komponen, React perlu memutuskan bagian pohon mana yang dipertahankan (dan diperbarui), serta bagian mana yang harus dibuang atau dibuat kembali dari awal. Pada kebanyakan kasus, perilaku otomatis React ini sudah cukup baik. Secara default, React akan mempertahankan bagian-bagian pohon yang “cocok” dengan struktur pohon yang sebelumnya telah di-render.
Namun, ada kalanya hal ini bukan yang Anda harapkan. Dalam contoh aplikasi obrolan ini, ketika Anda mengetik pesan dan kemudian mengubah penerima pesan, itu tidak mengatur ulang bidang masukan yang ada. Hal ini bisa membuat pengguna secara tidak sengaja mengirim pesan ke orang yang salah:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
React memungkinkan Anda untuk mengesampingkan perilaku default, dan memaksa sebuah komponen untuk mengatur ulang statusnya (state) dengan memberikan key
yang berbeda, seperti <Chat key={email} />
. Hal ini memberitahu React bahwa jika penerima berbeda, itu harus dianggap sebagai komponen Chat
yang berbeda yang perlu dibuat kembali dari awal dengan data (dan UI seperti input) yang baru. Sekarang, beralih antara penerima mengatur ulang input - meskipun Anda me-render komponen yang sama.
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.email} contact={to} /> </div> ) } const contacts = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
Siap mempelajari topik ini?
Baca Preserving and Resetting State untuk mempelajari masa hidup status dan cara mengendalikannya.
Baca Lebih LanjutMengekstrak logika state ke dalam reducer
Komponen dengan banyak pembaruan state yang tersebar di banyak event handler dapat menjadi sangat membingungkan. Untuk kasus-kasus ini, Anda dapat mengkonsolidasikan semua logika pembaruan state di luar komponen Anda dalam sebuah fungsi tunggal, yang disebut “reducer”. Event handler Anda menjadi lebih ringkas karena hanya menentukan “aksi” pengguna. Di bagian bawah file, fungsi reducer menentukan bagaimana state harus diperbarui sebagai respons terhadap setiap aksi!
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <> <h1>Rencana perjalanan Praha</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Mengunjungi Musium Kafka', done: true }, { id: 1, text: 'Menonton Pertujukan Boneka', done: false }, { id: 2, text: 'Foto Tembok Lennon', done: false } ];
Siap mempelajari topik ini?
Baca Extracting State Logic into a Reducer untuk mempelajari cara mengkonsolidasikan logika dalam fungsi reducer.
Baca Lebih LanjutMelewatkan data secara dalam dengan context
Biasanya, Anda akan melewatkan informasi dari komponen induk ke komponen anak (children) melalui props. Namun, melewatkan props dapat menjadi merepotkan jika Anda perlu melewatkan beberapa prop melalui banyak komponen, atau jika banyak komponen membutuhkan informasi yang sama. Context memungkinkan komponen induk membuat beberapa informasi tersedia untuk setiap komponen di bawahnya—tidak peduli seberapa dalam itu—tanpa melewatkan secara eksplisit melalui props.
Di sini, komponen Heading
menentukan tingkat judulnya dengan “bertanya” pada Section
terdekat untuk tingkatnya. Setiap Section
melacak tingkatnya sendiri dengan bertanya pada Section
induk dan menambahkan satu. Setiap Section
menyediakan informasi kepada semua komponen di bawahnya tanpa melewatkan props—itu dilakukan melalui context.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Judul</Heading> <Section> <Heading>Judul</Heading> <Heading>Judul</Heading> <Heading>Judul</Heading> <Section> <Heading>Sub-judul</Heading> <Heading>Sub-judul</Heading> <Heading>Sub-judul</Heading> <Section> <Heading>Sub-sub-judul</Heading> <Heading>Sub-sub-judul</Heading> <Heading>Sub-sub-judul</Heading> </Section> </Section> </Section> </Section> ); }
Siap mempelajari topik ini?
Baca Passing Data Deeply with Context untuk mempelajari penggunaan context sebagai alternatif dari melewatkan props.
Baca Lebih LanjutPeningkatan skala dengan reducer dan context
Reducer memungkinkan Anda mengonsolidasikan logika pembaruan state dari sebuah komponen. Context memungkinkan Anda melewatkan informasi ke komponen lain secara dalam. Anda dapat menggabungkan reducer dan context bersama-sama untuk mengelola state dari layar yang kompleks.
Dengan pendekatan ini, sebuah komponen induk dengan state yang kompleks dikelola dengan reducer. Komponen lain di dalam tree dapat membaca state-nya melalui context. Mereka juga dapat melakukan dispatch tindakan untuk memperbarui state tersebut.
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Hari libur di Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
Siap mempelajari topik ini?
Baca Peningkatan Skala dengan Reducer dan Context untuk mempelajari bagaimana pengelolaan state mengembang pada aplikasi yang berkembang.
Baca Lebih LanjutApa selanjutnya?
Lanjut ke halaman Reacting to Input with State untuk mulai membaca bab ini halaman per halaman!
Atau, jika Anda sudah familiar dengan topik-topik ini, mengapa tidak membaca tentang Escape Hatches?