Debugging React — Debugging

Debugging React Applications React memiliki tantangan debugging unik karena virtual DOM, state management, dan component lifecycle. Berikut tools dan teknik…

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: App punya state klik. Di dalamnya ada List dengan dua Item, dan props mereka gak berubah sama sekali. Kamu klik +1 di App. Berapa komponen yang render ulang: 1 (cuma App) 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.

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>
  );
}

Profiler: siapa yang render, dan kenapa

  1. Buka tab Profiler, klik ikon gear, lalu centang Record why each component rendered while profiling.
  2. Klik tombol rekam (lingkaran biru), klik +1 sekali di app, lalu stop.
  3. 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)

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

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari