Mempertahankan dan Mengatur Ulang State
State diisolasi antar komponen. React melacak state mana yang dimiliki oleh komponen mana berdasarkan tempatnya di pohon antarmuka pengguna (UI). Anda dapat mengontrol kapan harus mempertahankan state dan kapan harus mengatur ulang di antara render ulang (re-render).
Anda akan mempelajari
- Bagaimana React “melihat” struktur komponen
- Kapan React memilih untuk mempertahankan atau mengatur ulang state
- Bagaimana cara memaksa React untuk mengatur ulang state komponen
- Bagaimana keys dan types mempengaruhi apakah state dipertahankan
Pohon antarmuka pengguna (UI)
Peramban menggunakan banyak struktur pohon untuk memodelkan antarmuka pengguna (UI). DOM mewakili elemen HTML, CSSOM melakukan hal yang sama untuk CSS. Bahkan ada Pohon aksesibilitas!
React juga menggunakan struktur pohon untuk mengelola dan memodelkan UI yang Anda buat. React membuat pohon UI dari JSX Anda. Kemudian React DOM memperbarui elemen-elemen DOM peramban agar sesuai dengan pohon UI tersebut (React Native menerjemahkan pohon-pohon tersebut menjadi elemen-elemen yang spesifik untuk platform mobile).
State terikat dengan posisi di dalam pohon
Ketika Anda memberikan state pada sebuah komponen, Anda mungkin berpikir bahwa state tersebut “hidup” di dalam komponen. Tetapi state sebenarnya disimpan di dalam React. React mengasosiasikan setiap bagian dari state yang dipegangnya dengan komponen yang benar berdasarkan posisi komponen tersebut di dalam pohon UI.
Di sini, hanya ada satu tag JSX <Counter />
, tetapi tag tersebut dirender pada dua posisi yang berbeda:
import { useState } from 'react'; export default function App() { const counter = <Counter />; return ( <div> {counter} {counter} </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Beginilah tampilannya sebagai pohon:
Ini adalah dua penghitung yang terpisah karena masing-masing di-render pada posisinya sendiri di dalam pohon. Anda biasanya tidak perlu memikirkan posisi-posisi ini untuk menggunakan React, tetapi akan sangat berguna untuk memahami cara kerjanya.
Dalam React, setiap komponen pada layar memiliki state yang terisolasi sepenuhnya. Sebagai contoh, jika Anda me-render dua komponen Counter
secara berdampingan, masing-masing komponen akan mendapatkan state-nya sendiri-sendiri, independen, yaitu state score
dan hover
.
Coba klik kedua penghitung dan perhatikan bahwa keduanya tidak saling mempengaruhi:
import { useState } from 'react'; export default function App() { return ( <div> <Counter /> <Counter /> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Seperti yang dapat Anda lihat, ketika satu penghitung diperbarui, hanya state untuk komponen tersebut yang diperbarui:
React akan mempertahankan state selama Anda me-render komponen yang sama pada posisi yang sama. Untuk melihat hal ini, naikkan kedua penghitung, lalu hapus komponen kedua dengan menghapus centang pada checkbox “Render the second counter”, lalu tambahkan kembali dengan mencentangnya lagi:
import { useState } from 'react'; export default function App() { const [showB, setShowB] = useState(true); return ( <div> <Counter /> {showB && <Counter />} <label> <input type="checkbox" checked={showB} onChange={e => { setShowB(e.target.checked) }} /> Render the second counter </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Perhatikan bagaimana saat Anda berhenti me-render penghitung kedua, state-nya akan hilang sepenuhnya. Hal ini dikarenakan ketika React menghapus sebuah komponen, ia akan menghancurkan state-nya.
Ketika Anda mencentang “Render the second counter”, Counter
kedua dan state-nya diinisialisasi dari awal (score = 0
) dan ditambahkan ke DOM.
React mempertahankan state sebuah komponen selama komponen tersebut di-render pada posisinya di pohon UI. Jika komponen tersebut dihapus, atau komponen lain di-render pada posisi yang sama, React akan membuang state-nya.
Komponen yang sama pada posisi yang sama mempertahankan state
Pada contoh ini, terdapat dua tag <Counter />
yang berbeda:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <Counter isFancy={true} /> ) : ( <Counter isFancy={false} /> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Ketika Anda mencentang atau menghapus checkbox, state penghitung tidak diatur ulang. Entah isFancy
bernilai true
atau false
, Anda selalu memiliki <Counter />
sebagai anak pertama dari div
yang dikembalikan dari komponen akar App
:
Ini adalah komponen yang sama pada posisi yang sama, jadi dari sudut pandang React, ini adalah penghitung yang sama.
Komponen yang berbeda pada posisi state reset yang sama
Pada contoh ini, mencentang checkbox akan menggantikan <Counter>
dengan <p>
:
import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>See you later!</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Take a break </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Di sini, Anda beralih di antara jenis komponen yang berbeda pada posisi yang sama. Awalnya, anak pertama dari <div>
berisi sebuah Counter
. Namun ketika Anda menukar p
, React menghapus Counter
dari pohon UI dan menghancurkan state-nya.
Selain itu, ketika Anda merender komponen yang berbeda pada posisi yang sama, komponen tersebut akan mengatur ulang state seluruh subpohonnya. Untuk melihat cara kerjanya, tingkatkan penghitungnya, lalu centang checkbox:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <section> <Counter isFancy={false} /> </section> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
State penghitung akan diatur ulang saat Anda mengklik checkbox. Meskipun Anda me-render Counter
, anak pertama dari div
berubah dari div
menjadi section
. Ketika anak div
dihapus dari DOM, seluruh pohon di bawahnya (termasuk Counter
dan state-nya) juga dihancurkan.
Sebagai aturan praktis, jika Anda ingin mempertahankan state di antara render ulang, struktur pohon Anda harus “cocok” dari satu render ke render lainnya. Jika strukturnya berbeda, state akan dihancurkan karena React menghancurkan state ketika menghapus sebuah komponen dari pohon.
Mengatur ulang state pada posisi yang sama
Secara default, React mempertahankan state dari sebuah komponen ketika komponen tersebut berada pada posisi yang sama. Biasanya, ini adalah hal yang Anda inginkan, sehingga masuk akal jika ini menjadi perilaku default. Namun terkadang, Anda mungkin ingin mengatur ulang state sebuah komponen. Pertimbangkan aplikasi ini yang memungkinkan dua pemain melacak skor mereka selama setiap giliran:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter person="Taylor" /> ) : ( <Counter person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Saat ini, ketika Anda mengganti pemain, skor tetap dipertahankan. Kedua Counter
muncul di posisi yang sama, sehingga React melihat mereka sebagai Counter
yang sama yang mana props person
telah berubah.
Namun secara konseptual, dalam aplikasi ini mereka seharusnya menjadi dua penghitung yang terpisah. Mereka mungkin muncul di tempat yang sama di UI, tetapi yang satu adalah penghitung untuk Taylor, dan yang lainnya adalah penghitung untuk Sarah.
Ada dua opsi untuk mengatur ulang state ketika beralih di antara keduanya:
- Merender komponen dalam posisi yang berbeda
- Berikan setiap komponen identitas eksplisit dengan
key
Opsi 1: Me-render komponen pada posisi yang berbeda
Jika Anda ingin kedua Counter
ini independen, Anda dapat membuat mereka dalam dua posisi yang berbeda:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA && <Counter person="Taylor" /> } {!isPlayerA && <Counter person="Sarah" /> } <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
- Awalnya,
isPlayerA
adalahtrue
. Jadi posisi pertama berisi stateCounter
, dan posisi kedua kosong. - Ketika Anda mengklik tombol “Next player”, posisi pertama akan hilang, namun posisi kedua sekarang berisi
Counter
.
Setiap state Counter
akan dihancurkan setiap kali dihapus dari DOM. Inilah sebabnya mengapa mereka mengatur ulang setiap kali Anda mengklik tombol.
Solusi ini nyaman ketika Anda hanya memiliki beberapa komponen independen yang di-render di tempat yang sama. Dalam contoh ini, Anda hanya memiliki dua komponen, sehingga tidak merepotkan untuk me-render keduanya secara terpisah di JSX.
Opsi 2: Mengatur ulang state dengan key
Ada juga cara lain yang lebih umum untuk mengatur ulang state komponen.
Anda mungkin pernah melihat key
ketika merender list. Key tidak hanya untuk list! Anda dapat menggunakan key untuk membuat React membedakan antar komponen. Secara default, React menggunakan urutan di dalam induk (“penghitung pertama”, “penghitung kedua”) untuk membedakan antar komponen. Tetapi dengan key, Anda dapat memberi tahu React bahwa ini bukan hanya penghitung pertama, atau penghitung kedua, tetapi penghitung yang spesifik—sebagai contoh, penghitung Taylor. Dengan cara ini, React akan mengetahui penghitung Taylor di mana pun dia muncul di dalam pohon!
Pada contoh ini, kedua <Counter />
tidak berbagi state meskipun keduanya muncul di tempat yang sama di JSX:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter key="Taylor" person="Taylor" /> ) : ( <Counter key="Sarah" person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Beralih antara Taylor dan Sarah tidak akan mempertahankan state. Ini karena Anda memberi mereka key
yang berbeda:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
Menentukan sebuah key
memberitahu React untuk menggunakan key
itu sendiri sebagai bagian dari posisi, bukan urutan mereka di dalam induk. Inilah sebabnya, meskipun Anda me-render mereka di tempat yang sama di JSX, React melihat mereka sebagai dua penghitung yang berbeda, sehingga mereka tidak akan pernah berbagi state. Setiap kali penghitung muncul di layar, state-nya dibuat. Setiap kali dihapus, state-nya akan dihancurkan. Mengalihkan di antara keduanya akan mengatur ulang state mereka berulang kali.
Mengatur ulang formulir dengan tombol
Mengatur ulang state dengan tombol sangat berguna terutama ketika berurusan dengan formulir.
Dalam aplikasi obrolan ini, komponen <Chat>
berisi state masukan teks:
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 = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Coba masukkan sesuatu ke dalam input, lalu tekan “Alice” atau “Bob” untuk memilih penerima yang berbeda. Anda akan melihat bahwa state masukan dipertahankan karena <Chat>
di-render pada posisi yang sama di pohon.
Di banyak aplikasi, ini mungkin merupakan perilaku yang diinginkan, tetapi tidak di aplikasi obrolan! Anda tidak ingin membiarkan pengguna mengirim pesan yang telah mereka ketik ke orang yang salah karena klik yang tidak disengaja. Untuk memperbaikinya, tambahkan key
:
<Chat key={to.id} contact={to} />
Hal ini memastikan bahwa ketika Anda memilih penerima yang berbeda, komponen Chat
akan dibuat ulang dari awal, termasuk state apa pun di dalam pohon di bawahnya. React juga akan membuat ulang elemen DOM daripada menggunakannya kembali.
Sekarang, mengganti penerima selalu mengosongkan bidang teks:
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.id} contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Pendalaman
Dalam aplikasi obrolan yang sebenarnya, Anda mungkin ingin memulihkan state masukan ketika pengguna memilih penerima sebelumnya lagi. Ada beberapa cara untuk menjaga state “hidup” untuk komponen yang tidak lagi terlihat:
- Anda dapat merender semua obrolan, bukan hanya obrolan yang sedang berlangsung, tetapi menyembunyikan semua obrolan lainnya dengan CSS. Obrolan tidak akan dihapus dari pohon, sehingga state lokalnya akan dipertahankan. Solusi ini bekerja dengan baik untuk UI yang sederhana. Tetapi ini bisa menjadi sangat lambat jika pohon yang disembunyikan berukuran besar dan berisi banyak simpul DOM.
- Anda dapat mengangkat state dan menyimpan pesan yang tertunda untuk setiap penerima di komponen induk. Dengan cara ini, ketika komponen anak dihapus, tidak menjadi masalah, karena induklah yang menyimpan informasi penting. Ini adalah solusi yang paling umum.
- Anda juga dapat menggunakan sumber yang berbeda selain state React. Sebagai contoh, Anda mungkin ingin draf pesan tetap ada meskipun pengguna tidak sengaja menutup halaman. Untuk mengimplementasikan hal ini, Anda dapat membuat komponen
Chat
menginisialisasi state-nya dengan membaca darilocalStorage
, dan menyimpan draft di sana juga.
Apapun strategi yang Anda pilih, obrolan dengan Alice secara konseptual berbeda dengan obrolan dengan Bob, sehingga masuk akal untuk memberikan key
pada pohon <Chat>
berdasarkan penerima saat ini.
Rekap
- React menyimpan state selama komponen yang sama di-render pada posisi yang sama.
- State tidak disimpan dalam tag JSX. Hal ini terkait dengan posisi pohon tempat Anda meletakkan JSX tersebut.
- Anda dapat memaksa subpohon untuk mengatur ulang state-nya dengan memberikan key yang berbeda.
- Jangan membuat sarang definisi komponen, atau Anda akan mengatur ulang state secara tidak sengaja.
Tantangan 1 dari 5: Memperbaiki teks masukan yang menghilang
Contoh ini menunjukkan pesan apabila Anda menekan tombol. Namun, menekan tombol juga secara tidak sengaja mengatur ulang masukan. Mengapa hal ini bisa terjadi? Perbaiki agar penekanan tombol tidak mengatur ulang teks masukan.
import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>Hint: Your favorite city?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>Hide hint</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>Show hint</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }