Seiring dengan bertumbuhnya aplikasi Anda, penting untuk lebih memperhatikan bagaimana state Anda diorganisasi dan bagaimana data mengalir di antara komponen-komponen Anda. State yang redundan atau duplikat adalah sumber umum dari bug. Di bab ini, Anda akan belajar bagaimana menstrukturkan state dengan baik, bagaimana menjaga logika pembaruan state agar mudah dikelola, dan bagaimana berbagi state antara komponen-komponen yang jauh.
In this chapter
- Bagaimana memikirkan perubahan UI sebagai perubahan state
- Bagaimana mengatur state dengan baik
- Bagaimana “mengangkat state ke atas” untuk dibagikan antara komponen-komponen
- Bagaimana mengontrol apakah state akan dipertahankan atau direset
- Bagaimana menggabungkan logika state yang kompleks dalam sebuah fungsi
- Bagaimana mengirimkan informasi tanpa ”prop drilling”
- Bagaimana meningkatkan manajemen state saat aplikasi Anda berkembang
Merespon masukan dengan State
Dalam React, Anda tidak akan mengubah UI dari kode secara langsung. Misalnya, Anda tidak akan menulis perintah seperti “nonaktifkan tombol”, “aktifkan tombol”, “tampilkan pesan sukses”, dll. Sebaliknya, Anda akan menggambarkan UI yang ingin Anda lihat untuk berbagai states visual dari komponen Anda (”state awal”, ”state mengetik”, ”state sukses”), dan kemudian memicu perubahan state sebagai respons terhadap masukan pengguna. Hal ini mirip dengan bagaimana desainer memikirkan tentang UI.
Berikut adalah sebuah 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) { // 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 tetapi jawaban salah. Silahkan coba lagi!')); } else { resolve(); } }, 1500); }); }
Ready to learn this topic?
Baca Reacting to Input with State untuk belajar bagaimana mendekati interaksi dengan mindset yang didorong oleh state.
Read MoreMemilih Struktur State
Mengatur struktur state dengan baik dapat membuat perbedaan antara komponen yang mudah dimodifikasi dan didebug, dan komponen yang menjadi sumber kesalahan yang konstan. Prinsip paling penting adalah bahwa state tidak boleh mengandung informasi yang tidak perlu atau duplikat. Jika ada state yang tidak perlu, mudah untuk lupa untuk memperbarui state tersebut, dan memperkenalkan kesalahan!
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 menghapusnya 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> </> ); }
Ini mungkin terlihat seperti perubahan kecil, tetapi banyak bug pada aplikasi React dapat diperbaiki dengan cara ini.
Ready to learn this topic?
Baca Choosing the State Structure untuk belajar cara merancang bentuk state untuk menghindari kesalahan (bugs).
Read MoreBerbagi State Antar Komponen
Terkadang, Anda ingin state dari dua komponen selalu berubah bersama. Untuk melakukannya, hapus state dari keduanya, pindahkan state tersebut ke induk (parent) paling dekat yang bersamaan, dan kemudian teruskan ke kedua komponen melalui props. Hal ini dikenal sebagai “mengangkat state ke atas” (lifting state up), dan ini adalah salah satu hal yang paling umum yang akan Anda lakukan saat menulis kode React.
Pada contoh ini, hanya satu panel yang harus aktif pada satu waktu. Untuk mencapainya, daripada menyimpan state aktif di setiap panel secara individu, komponen parent 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> ); }
Ready to learn this topic?
Baca Berbagi State Antar Komponen untuk mempelajari cara mengangkat state ke atas dan menjaga sinkronisasi antar komponen.
Read MoreMemperjelas dan Mengatur Ulang State
Saat Anda me-render ulang sebuah komponen, React perlu memutuskan bagian mana dari pohon untuk dipertahankan (dan diperbarui), serta bagian mana yang harus dibuang atau dibuat kembali dari awal. Pada kebanyakan kasus, perilaku otomatis React sudah cukup baik. Secara default, React mempertahankan bagian-bagian pohon yang “cocok” dengan pohon komponen yang sebelumnya di-render.
Namun, terkadang ini bukan yang Anda inginkan. Dalam aplikasi obrolan ini, mengetik pesan dan kemudian mengubah penerima tidak akan mengatur ulang input. Hal ini dapat 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' } ];
Ready to learn this topic?
Baca Preserving and Resetting State untuk mempelajari masa hidup status dan cara mengendalikannya.
Read MoreMengekstrak 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 } ];
Ready to learn this topic?
Baca Extracting State Logic into a Reducer untuk mempelajari cara mengkonsolidasikan logika dalam fungsi reducer.
Read MoreMelewatkan 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> ); }
Ready to learn this topic?
Baca Passing Data Deeply with Context untuk mempelajari penggunaan context sebagai alternatif dari melewatkan props.
Read MorePeningkatan 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> ); }
Ready to learn this topic?
Baca Peningkatan Skala dengan Reducer dan Context untuk mempelajari bagaimana pengelolaan state mengembang pada aplikasi yang berkembang.
Read MoreApa 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?