Debugging React artinya memeriksa komponen, bukan cuma HTML-nya: komponen mana memegang props dan state apa, dan kenapa ia render ulang. Bayangin dapur restoran. Panel Elements cuma memperlihatkan piring yang sudah tersaji (DOM). React Developer Tools membuka pintu dapurnya: koki mana (komponen) memasak bagian mana, dengan bahan apa dari kepala koki (props), dan apa isi laci miliknya sendiri (state). Profiler-nya mencatat tiap pesanan dan koki mana saja yang ikut memasak, termasuk koki yang memasak ulang hidangan yang persis sama tanpa alasan.
🧪 Tebakan cepat:
Apppunya stateklik. Di dalamnya adaListdengan duaItem, dan props mereka gak berubah sama sekali. Kamu klik +1 diApp. Berapa komponen yang render ulang: 1 (cumaApp) atau 4? Tebak dulu, jawabannya ada di bagian "Profiler: siapa yang render, dan kenapa".
React Developer Tools
React Developer Tools adalah extension browser (Chrome Web Store atau Firefox Add-ons). Setelah dipasang, DevTools punya dua tab baru di halaman yang memakai React: Components dan Profiler.
- Components menampilkan pohon komponen, bukan pohon DOM:
App, di dalamnyaList, di dalamnyaItem. Klik satu komponen, dan panel kanan menampilkan props-nya, hooks-nya (state dariuseStatetampil di sini sebagaiState), dan rendered by (komponen yang merender-nya). - Nilai props dan state bisa kamu ubah langsung di panel itu, untuk mencoba tampilan dengan data lain tanpa mengedit kode.
- Profiler merekam render selama kamu berinteraksi, lalu menunjukkan komponen mana yang render di tiap commit dan alasannya. Commit adalah satu kali React memperbarui layar.
App yang kita periksa
Ini app di Lab. App menyimpan dua state: jumlah klik dan daftar barang.
function Item({ id, nama, harga }) {
return <li>{nama}: {harga}</li>;
}
function List({ items }) {
return <ul>{items.map((item) => <Item key={item.id} {...item} />)}</ul>;
}
function App() {
const [klik, setKlik] = useState(0);
const [items, setItems] = useState([
{ id: 1, nama: "Buku", harga: 50000 },
{ id: 2, nama: "Pensil", harga: 5000 },
]);
return (
<div>
<button onClick={() => setKlik(klik + 1)}>+1 ({klik})</button>
<List items={items} />
</div>
);
}
klikcuma dipakaiAppsendiri.ListdanItemgak pernah membacanya.itemsdisimpan sebagai state, jadi array-nya tetap object yang sama di antara render, sampaisetItemsdipanggil.
Profiler: siapa yang render, dan kenapa
- Buka tab Profiler, klik ikon gear, lalu centang Record why each component rendered while profiling.
- Klik tombol rekam (lingkaran biru), klik +1 sekali di app, lalu stop.
- Pilih satu commit, arahkan kursor ke komponen, dan baca Why did this render?
Hasil rekaman app tadi, dari halaman dimuat sampai satu klik:
| Commit | Komponen | Why did this render? | Ditulis di log latihan |
|---|---|---|---|
| 1 | App |
This is the first time the component rendered. | mount |
| 1 | List |
This is the first time the component rendered. | mount |
| 1 | Item id 1 dan id 2 |
This is the first time the component rendered. | mount |
| 2 | App |
Hook 1 changed | state |
| 2 | List |
The parent component rendered. | parent |
| 2 | Item id 1 |
The parent component rendered. | parent |
| 2 | Item id 2 |
The parent component rendered. | parent |
Satu klik bikin 4 komponen render. Itu jawaban tebakannya. Aturan dasarnya: kalau satu komponen render, semua anaknya ikut render, walaupun props mereka sama persis. "Hook 1 changed" adalah cara DevTools bilang state useState pertama berubah.
List dan dua Item di commit 2 adalah render sia-sia (wasted render): alasannya cuma "parent rendered", dan props serta state-nya sama persis dengan render sebelumnya, jadi hasilnya pun sama. Dengan dua item gak terasa. Dengan 2.000 baris tabel, tiap klik yang gak ada hubungannya bisa bikin halaman tersendat.
React.memo: lewati render yang sia-sia
memo membungkus komponen supaya React melewatinya kalau semua props-nya sama dengan render sebelumnya.
import { memo } from "react";
const Item = memo(function Item({ id, nama, harga }) {
return <li>{nama}: {harga}</li>;
});
const List = memo(function List({ items }) {
return <ul>{items.map((item) => <Item key={item.id} {...item} />)}</ul>;
});
| Aksi | Tanpa memo |
Dengan memo |
|---|---|---|
| Klik +1 | App, List, Item 1, Item 2 |
App saja |
Ubah harga Pensil lewat setItems |
App, List, Item 1, Item 2 |
App, List (Props changed: items), Item 2 (Props changed: harga) |
memomembandingkan tiap prop denganObject.is: angka dan teks dibandingkan isinya, object, array, dan function dibandingkan identitasnya.- Mengubah harga lewat
setItemsmembuat array baru, jadi propsListberubah danListtetap render.ItemBuku tetap dilewati karena props-nya sama. - Object atau function baru yang dibuat di tiap render (
style={{ color: "red" }},onClick={() => ...}) bikinmemoselalu gagal. Simpan denganuseMemoatauuseCallbackkalau memang perlu. - Jangan bungkus semuanya. Ukur dulu di Profiler, lalu
memo-kan komponen yang sering render sia-sia dan mahal.
Latihan: menganalisis log Profiler
Di latihan, log Profiler sudah disiapkan sebagai array. Tiap catatan berbentuk { component, duration, reason, props, state }, dengan duration dalam milidetik dan reason salah satu dari mount, props, state, parent, hooks. Lognya 11 catatan: mount, satu klik, lalu klik kedua.
| Yang dihitung | Caranya | Hasil |
|---|---|---|
totalDuration |
jumlah semua duration, dibulatkan |
49 |
byReason.mount |
banyaknya catatan mount |
4 |
byReason.parent |
banyaknya catatan parent |
5 |
slowestComponent |
jumlahkan duration per komponen: App 13, List 26, Item 10 |
List |
findWastedRenders |
catatan parent yang props dan state-nya sama dengan catatan sebelumnya dari komponen yang sama |
Item,List |
recommendMemo(log, 2) |
komponen dengan minimal 2 render sia-sia: List 2 kali, Item 3 kali |
Item,List |
Ada dua Item (id 1 dan id 2). Karena itu render Item dibandingkan dengan salah satu catatan Item sebelumnya yang props-nya persis sama, bukan cuma catatan Item tepat sebelumnya. Bandingkan props dan state dengan JSON.stringify(a) === JSON.stringify(b).
Bug klasik: render tanpa henti
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // jalan sesudah tiap render, lalu memicu render lagi
}); // tanpa dependency array
Effect tanpa dependency array jalan sesudah setiap render. Ia mengubah state, state memicu render, render memicu effect lagi, terus-menerus. Di mode development React menulis peringatan:
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Perbaikannya: beri dependency array yang benar ([] untuk sekali saat mount, [userId] untuk saat userId berubah), atau jangan simpan sebagai state sama sekali kalau nilainya bisa dihitung langsung dari props. Kalau setCount(...) dipanggil langsung di badan komponen (bukan di handler atau effect), React menghentikannya dengan error Too many re-renders. React limits the number of renders to prevent an infinite loop.
🎯 Trik lain saat debugging React
- Di Components, buka gear lalu nyalakan Highlight updates when components render. Tiap komponen yang render akan berkedip di halaman, jadi render sia-sia kelihatan tanpa merekam.
console.logdi badan komponen bisa muncul dua kali di development. Itu karena<StrictMode>sengaja merender dua kali untuk menangkap kode yang gak murni, bukan bug. Di production cuma sekali.- Peringatan
Each child in a list should have a unique "key" prop.di Console artinya elemen hasilmapbelum diberikeyyang unik dan stabil, misalnyakey={item.id}.
⚠️ Jebakan yang sering ketemu
- Mencari state di panel Elements. Elements cuma tahu HTML. Props dan state ada di tab Components.
- Mengira anak cuma render kalau props-nya berubah. Tanpa
memo, anak ikut render setiap kali induknya render. memodikalahkan object atau function baru. Props yang dibuat ulang tiap render selalu dianggap berubah.- Effect tanpa dependency array yang memanggil setState. Itu loop render tanpa henti.
- Di latihan: membandingkan object dengan
===.{ id: 1 } === { id: 1 }hasilnyafalse. PakaiJSON.stringifyuntuk props dan state, dan urutkan nama hasilnya secara alfabet.
TL;DR: Pasang React Developer Tools: tab Components menunjukkan pohon komponen beserta props dan hooks-nya (bisa diedit langsung), tab Profiler merekam siapa yang render di tiap commit dan kenapa (nyalakan "Record why each component rendered while profiling."). Kalau induk render, semua anaknya ikut render; render dengan alasan "The parent component rendered." dan props yang sama persis adalah render sia-sia. memo melewati komponen yang props-nya sama (dibandingkan dengan Object.is), tapi kalah oleh object dan function baru di tiap render. useEffect tanpa dependency array yang memanggil setState bikin render tanpa henti. Di latihan: total durasi 49, mount 4, parent 5, komponen terlama List, dan render sia-sia ada di Item dan List.