Bab 3: Render dan Commit

7 menit baca

Analogi: Restoran

Bayangin proses di restoran:

  1. Trigger — Pelanggan pesan makanan (ada "pemicu" yang memulai proses)
  2. Render — Koki masak di dapur (proses persiapan di belakang layar)
  3. Commit — Pelayan antar makanan ke meja (hasil akhir ditampilkan ke pelanggan)

React bekerja persis kayak gini. Ada 3 tahap yang terjadi setiap kali tampilan berubah:

  1. Trigger — Sesuatu memicu render (pertama kali muncul, atau state berubah)
  2. Render — React memanggil komponen kamu untuk menentukan apa yang harus ditampilkan
  3. Commit — React menerapkan perubahan ke DOM (layar yang user lihat)

Mari kita bahas satu per satu secara mendalam.


Tahap 1: Trigger (Pemicu Render)

Ada dua situasi yang memicu render:

A. Initial Render (Render Pertama)

Saat aplikasi pertama kali dimulai, React perlu me-render semua komponen untuk pertama kalinya. Ini terjadi saat kamu memanggil createRoot dan .render():

jsx
import { createRoot } from 'react-dom/client';
import App from './App';

// Ini memicu initial render
const root = createRoot(document.getElementById('root'));
root.render(<App />);

Analoginya: ini kayak restoran baru buka. Semua meja perlu disiapkan dari nol. Belum ada makanan di meja manapun.

B. Re-render (Render Ulang)

Setelah initial render, render selanjutnya dipicu oleh perubahan state (memanggil setState):

jsx
import { useState } from 'react';

function Counter() {
  const [angka, setAngka] = useState(0);

  // Setiap kali setAngka dipanggil = trigger re-render
  return (
    <button onClick={() => setAngka(angka + 1)}>
      Angka: {angka}
    </button>
  );
}

Analoginya: pelanggan pesan makanan tambahan. Koki perlu masak lagi, pelayan perlu antar lagi.

Penting: Mengubah variabel biasa TIDAK memicu re-render. Hanya setState yang bisa.

jsx
function BukanTrigger() {
  let angka = 0;
  
  function handleKlik() {
    angka = angka + 1; // Ini BUKAN trigger! React nggak tau!
    console.log(angka); // Naik di console, tapi layar nggak update
  }

  return <button onClick={handleKlik}>Angka: {angka}</button>;
}

Tahap 2: Render (React Memanggil Komponen)

Setelah trigger terjadi, React "memanggil" komponen kamu. Apa artinya "memanggil"? Ingat, komponen React itu cuma fungsi. Jadi "render" = React menjalankan fungsi komponen kamu.

jsx
function Salam() {
  // Setiap kali React "render" komponen ini,
  // dia menjalankan seluruh kode di fungsi ini
  const waktu = new Date().toLocaleTimeString();
  
  return <p>Halo! Sekarang jam {waktu}</p>;
}

Initial Render

Saat initial render, React memanggil komponen root (biasanya <App />). Dari situ, kalau App me-return komponen lain, React juga memanggil komponen-komponen itu. Proses ini berlanjut sampai semua komponen sudah dipanggil.

jsx
function App() {
  return (
    <div>
      <Header />    {/* React panggil Header() */}
      <Content />   {/* React panggil Content() */}
      <Footer />    {/* React panggil Footer() */}
    </div>
  );
}

function Header() {
  return <h1>Warung Kopi</h1>;  // React catat: perlu bikin <h1>
}

function Content() {
  return <p>Selamat datang!</p>; // React catat: perlu bikin <p>
}

function Footer() {
  return <small>© 2024</small>; // React catat: perlu bikin <small>
}

Re-render

Saat re-render, React memanggil komponen yang state-nya berubah. Lalu, kalau komponen itu me-return komponen child, React juga memanggil child-child itu (secara rekursif).

jsx
function Parent() {
  const [angka, setAngka] = useState(0);
  
  // Kalau angka berubah, Parent di-render ulang
  // Dan Child juga di-render ulang (karena dia di dalam Parent)
  return (
    <div>
      <p>Parent: {angka}</p>
      <button onClick={() => setAngka(angka + 1)}>+1</button>
      <Child />
    </div>
  );
}

function Child() {
  // Ini juga dipanggil ulang saat Parent re-render
  console.log('Child di-render');
  return <p>Saya child</p>;
}

Catatan penting: Rendering itu proses kalkulasi. React cuma menghitung apa yang SEHARUSNYA ada di layar. Dia belum menyentuh DOM yang sebenarnya. Itu terjadi di tahap commit.

Analoginya: koki masak di dapur. Dia belum antar ke meja. Dia cuma nyiapin makanannya dulu.


Tahap 3: Commit (Menerapkan ke DOM)

Setelah render selesai (React sudah tau apa yang harus ditampilkan), React menerapkan perubahan ke DOM.

Initial Render

Untuk render pertama, React pakai appendChild() untuk memasukkan semua elemen DOM yang baru dibuat ke halaman.

jsx
// Hasil render: <h1>Halo</h1>
// React: document.body.appendChild(h1Element)

Re-render

Untuk re-render, React menerapkan perubahan minimal yang diperlukan. React cuma mengubah bagian DOM yang benar-benar berbeda dari render sebelumnya.

jsx
function Jam() {
  const [waktu, setWaktu] = useState(new Date());

  // Setiap detik, update waktu
  // (ini contoh sederhana, nanti belajar useEffect yang lebih proper)
  
  return (
    <div>
      <h1>Jam Digital</h1>           {/* Nggak berubah — React SKIP */}
      <p>{waktu.toLocaleTimeString()}</p>  {/* Berubah — React UPDATE */}
      <small>Zona: WIB</small>       {/* Nggak berubah — React SKIP */}
    </div>
  );
}

React cuma update teks di dalam <p>, nggak sentuh <h1> atau <small> karena mereka nggak berubah. Ini yang bikin React efisien.

Analoginya: pelayan nggak ganti semua piring di meja. Dia cuma ganti piring yang isinya berubah (misalnya nasi habis, diganti nasi baru). Piring yang masih oke dibiarkan.


Virtual DOM dan Diffing

Kamu mungkin pernah dengar istilah "Virtual DOM". Ini konsep kunci yang bikin React cepat.

Apa itu Virtual DOM?

Virtual DOM itu representasi DOM dalam bentuk JavaScript object. Kayak "blueprint" atau "denah" dari tampilan.

jsx
// JSX ini:
<div className="card">
  <h2>Kopi Susu</h2>
  <p>Rp 25.000</p>
</div>

// Di belakang layar jadi object kayak gini (disederhanakan):
{
  type: 'div',
  props: { className: 'card' },
  children: [
    { type: 'h2', children: 'Kopi Susu' },
    { type: 'p', children: 'Rp 25.000' }
  ]
}

Proses Diffing

Saat re-render, React bikin Virtual DOM baru, lalu membandingkan (diff) dengan Virtual DOM lama. Dari perbandingan ini, React tau persis apa yang berubah.

Render lama: Render baru: ┌─────────────────┐ ┌─────────────────┐ │ <div> │ │ <div> │ ← Sama │ <h2>Kopi</h2> │ │ <h2>Kopi</h2> │ ← Sama │ <p>Rp 25k</p>│ │ <p>Rp 30k</p>│ ← BEDA! │ </div> │ │ </div> │ ← Sama └─────────────────┘ └─────────────────┘ Hasil diff: Cuma update teks di <p> dari "Rp 25k" ke "Rp 30k"

Analoginya: bayangin kamu punya dua foto yang hampir identik (game "spot the difference"). React itu jago banget nyari bedanya. Dia cuma perbaiki bagian yang beda, nggak gambar ulang seluruh foto.

Kenapa Nggak Langsung Update DOM Aja?

Pertanyaan bagus! Kenapa React repot-repot bikin Virtual DOM, banding-bandingin, baru update? Kenapa nggak langsung update DOM?

Jawabannya: manipulasi DOM itu mahal (lambat). Setiap kali kamu ubah DOM, browser harus:

  1. Recalculate styles
  2. Reflow (hitung ulang layout)
  3. Repaint (gambar ulang pixel)

Kalau kamu update 100 elemen satu per satu, browser melakukan proses di atas 100 kali. Tapi kalau React kumpulkan semua perubahan dulu (di Virtual DOM), lalu apply sekaligus, browser cuma perlu proses sekali. Jauh lebih efisien.


Batching: React Mengelompokkan Update

React nggak langsung re-render setiap kali setState dipanggil. Dia mengelompokkan (batch) beberapa update jadi satu render.

jsx
function ContohBatching() {
  const [nama, setNama] = useState('');
  const [umur, setUmur] = useState(0);
  const [kota, setKota] = useState('');

  function handleKlik() {
    // Tiga setState dipanggil...
    setNama('Budi');
    setUmur(25);
    setKota('Jakarta');
    // ...tapi React cuma re-render SEKALI!
  }

  console.log('Komponen di-render'); // Cuma muncul 1x setelah klik

  return (
    <div>
      <p>{nama}, {umur}, {kota}</p>
      <button onClick={handleKlik}>Update Semua</button>
    </div>
  );
}

Kenapa batching? Performa! Bayangin kamu mau kirim 3 paket ke alamat yang sama. Lebih efisien kirim sekaligus dalam satu trip daripada bolak-balik 3 kali.

Kapan Batching Terjadi?

Di React 18+, batching terjadi di semua konteks:

  • Event handler (onClick, onChange, dll) ✅
  • setTimeout/setInterval ✅
  • Promise (.then) ✅
  • Native event listener ✅
jsx
function ContohBatchingLengkap() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  function handleKlik() {
    // Semua ini di-batch jadi 1 render
    setA(1);
    setB(2);
  }

  function handleAsync() {
    setTimeout(() => {
      // Di React 18+, ini juga di-batch!
      setA(10);
      setB(20);
      // Cuma 1 re-render
    }, 1000);
  }

  function handleFetch() {
    fetch('/api/data').then(res => res.json()).then(data => {
      // Ini juga di-batch di React 18+
      setA(data.a);
      setB(data.b);
      // Cuma 1 re-render
    });
  }

  return (
    <div>
      <p>a: {a}, b: {b}</p>
      <button onClick={handleKlik}>Sync Update</button>
      <button onClick={handleAsync}>Async Update</button>
      <button onClick={handleFetch}>Fetch Update</button>
    </div>
  );
}

Alur Lengkap: Dari Klik Sampai Tampilan Berubah

Mari kita trace alur lengkap saat user klik tombol:

jsx
import { useState } from 'react';

function App() {
  const [count, setCount] = useState(0);
  
  console.log('App di-render, count =', count);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

Alur saat tombol diklik:

1. [TRIGGER] User klik tombol → Event handler dipanggil → setCount(0 + 1) dipanggil → React menandai: "App perlu di-render ulang dengan count = 1" 2. [RENDER] React memanggil App() → useState(0) mengembalikan [1, setCount] (nilai tersimpan = 1) → console.log('App di-render, count = 1') → Return JSX: <div><p>Count: 1</p><button>...</button></div> → React bikin Virtual DOM baru 3. [DIFF] React bandingkan Virtual DOM lama vs baru → <div> sama ✓ → <p> isinya beda: "Count: 0" → "Count: 1" ← PERLU UPDATE → <button> sama ✓ 4. [COMMIT] React update DOM → Cuma update text node di dalam <p> → Browser repaint bagian yang berubah 5. [SELESAI] User lihat "Count: 1" di layar

Render Itu Pure (Murni)

Konsep penting: fungsi komponen harus pure (murni) saat render. Artinya:

  • Dengan input (props, state) yang sama, hasilnya selalu sama
  • Nggak boleh mengubah sesuatu yang sudah ada sebelum render
jsx
// ✅ Pure — hasil selalu sama untuk input yang sama
function Salam({ nama }) {
  return <h1>Halo, {nama}!</h1>;
}

// ❌ Tidak pure — mengubah variabel di luar
let hitungRender = 0;

function KomponenNakal() {
  hitungRender++; // Mengubah variabel luar saat render — JANGAN!
  return <p>Render ke-{hitungRender}</p>;
}

Kenapa harus pure? Karena React bisa memanggil komponen kamu berkali-kali (misalnya di Strict Mode, React render 2x untuk deteksi bug). Kalau komponen kamu punya side effect saat render, hasilnya bisa nggak konsisten.

Analoginya: resep masakan harus konsisten. Kalau resep bilang "2 sendok gula", hasilnya harus selalu sama. Nggak boleh resepnya tiba-tiba ngubah jumlah gula di resep orang lain.


Strict Mode dan Double Render

Di development, React Strict Mode memanggil komponen kamu dua kali setiap render. Ini bukan bug, ini fitur untuk mendeteksi masalah.

jsx
// Di index.js atau main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);
jsx
function App() {
  console.log('App di-render'); 
  // Di development + StrictMode: muncul 2x!
  // Di production: muncul 1x
  
  return <h1>Halo</h1>;
}

Jangan panik kalau lihat console.log muncul dua kali. Itu normal di development. Di production, cuma sekali.


Visualisasi Proses Render

┌─────────────────────────────────────────────────────────┐ │ REACT RENDER CYCLE │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ TRIGGER │───→│ RENDER │───→│ COMMIT │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ • Initial render • Panggil • Update DOM │ │ • setState() komponen • Cuma bagian │ │ • Hitung JSX yang berubah │ │ • Bikin Virtual │ │ DOM baru │ │ • Diff dengan │ │ yang lama │ │ │ └─────────────────────────────────────────────────────────┘

Contoh: Komponen yang Render Berkali-kali

jsx
import { useState } from 'react';

function PelacakRender() {
  const [angka, setAngka] = useState(0);
  
  // Ini dipanggil setiap render
  const waktuRender = new Date().toLocaleTimeString();
  console.log(`Render pada ${waktuRender}, angka = ${angka}`);

  return (
    <div>
      <p>Angka: {angka}</p>
      <p>Terakhir render: {waktuRender}</p>
      <button onClick={() => setAngka(angka + 1)}>Tambah</button>
      <button onClick={() => setAngka(angka)}>
        Set Nilai Sama (nggak trigger render)
      </button>
    </div>
  );
}

Perhatiin: kalau kamu set state ke nilai yang SAMA dengan sebelumnya, React mungkin skip re-render (optimisasi). Tapi jangan bergantung pada perilaku ini.


⚠️ Jebakan

Jebakan 1: Mengira render = update layar

jsx
function Salah() {
  const [angka, setAngka] = useState(0);

  function handleKlik() {
    setAngka(1);
    // Di sini, layar BELUM berubah!
    // Render baru dijadwalkan, belum terjadi
    console.log('Layar belum update di sini');
  }
}

Render itu asynchronous. Setelah setState, React menjadwalkan render, tapi belum langsung terjadi. Layar update setelah commit selesai.

Jebakan 2: Side effect di dalam render

jsx
// ❌ SALAH — side effect saat render
function KomponenBuruk() {
  // Jangan fetch data di sini! Ini dipanggil setiap render!
  fetch('/api/data'); // Ini bakal dipanggil berkali-kali!
  
  return <div>...</div>;
}

// ✅ BENAR — side effect di event handler atau useEffect
function KomponenBaik() {
  function handleKlik() {
    fetch('/api/data'); // OK di event handler
  }
  
  return <button onClick={handleKlik}>Fetch Data</button>;
}

Jebakan 3: Mengira setiap setState = 1 render

jsx
function Contoh() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  function handleKlik() {
    setA(1); // Nggak langsung render
    setB(2); // Nggak langsung render
    // React batch keduanya → 1 render total
  }
}

Jebakan 4: Bingung kenapa child ikut re-render

jsx
function Parent() {
  const [angka, setAngka] = useState(0);
  
  return (
    <div>
      <p>{angka}</p>
      <button onClick={() => setAngka(angka + 1)}>+1</button>
      <Child /> {/* Ini JUGA di-render ulang! */}
    </div>
  );
}

function Child() {
  console.log('Child render'); // Muncul setiap Parent re-render
  return <p>Saya child yang nggak berubah</p>;
}

Saat parent re-render, semua child-nya juga di-render ulang secara default. Ini biasanya nggak masalah (render itu cepat), tapi kalau jadi masalah performa, nanti bisa dioptimasi dengan React.memo.


🏋️ Challenge

Challenge 1: Prediksi Output

Tanpa menjalankan kode, prediksi urutan console.log yang muncul saat tombol diklik:

jsx
function Quiz() {
  const [angka, setAngka] = useState(0);
  
  console.log('A: Render, angka =', angka);

  function handleKlik() {
    console.log('B: Sebelum setState');
    setAngka(1);
    console.log('C: Setelah setState');
  }

  console.log('D: Selesai render');

  return <button onClick={handleKlik}>Klik</button>;
}
💡 Hint

Ingat: setState TIDAK langsung memicu render. Render terjadi SETELAH event handler selesai. Dan saat render, seluruh fungsi komponen dijalankan dari atas ke bawah.

✅ Solusi

Saat initial render (sebelum klik):

A: Render, angka = 0 D: Selesai render

Saat tombol diklik:

B: Sebelum setState C: Setelah setState A: Render, angka = 1 D: Selesai render

Penjelasan:

  1. Initial render: fungsi dijalankan dari atas ke bawah → A lalu D
  2. Klik tombol: handler dijalankan → B lalu C
  3. Setelah handler selesai, React re-render → A (dengan angka=1) lalu D

Perhatiin: C muncul SEBELUM render baru. Ini membuktikan setState nggak langsung trigger render.

Challenge 2: Hitung Jumlah Render

Buat komponen yang menampilkan berapa kali dia sudah di-render. (Hint: kamu butuh sesuatu yang bertahan antar render tapi TIDAK memicu render... ini tricky!)

💡 Hint

Kamu bisa pakai useRef untuk menyimpan nilai yang bertahan antar render tanpa memicu re-render. Atau, untuk challenge ini, cukup pakai variabel di luar komponen (module-level variable).

✅ Solusi
jsx
import { useState, useRef } from 'react';

function PenghitungRender() {
  const jumlahRender = useRef(0);
  const [angka, setAngka] = useState(0);

  // Setiap kali fungsi ini dipanggil = 1 render
  jumlahRender.current++;

  return (
    <div>
      <p>Komponen ini sudah di-render {jumlahRender.current} kali</p>
      <p>Angka: {angka}</p>
      <button onClick={() => setAngka(angka + 1)}>
        Tambah (trigger re-render)
      </button>
    </div>
  );
}

Catatan: useRef belum dibahas detail di part ini. Untuk sekarang, cukup tau bahwa useRef menyimpan nilai yang bertahan antar render tanpa memicu re-render. Beda dengan state yang memicu re-render saat berubah.

Challenge 3: Optimasi Render

Perhatikan kode berikut. Berapa kali Child di-render saat tombol diklik sekali? Bagaimana cara menguranginya?

jsx
function Parent() {
  const [angka, setAngka] = useState(0);

  return (
    <div>
      <p>Angka: {angka}</p>
      <button onClick={() => setAngka(angka + 1)}>+1</button>
      <Child nama="Budi" />
      <Child nama="Ani" />
      <Child nama="Citra" />
    </div>
  );
}

function Child({ nama }) {
  console.log(`${nama} di-render`);
  return <p>Halo, {nama}!</p>;
}
💡 Hint

Secara default, semua child di-render ulang saat parent re-render. Untuk mencegah ini, kamu bisa pakai React.memo() yang membungkus child dan hanya re-render kalau props-nya berubah.

✅ Solusi

Saat tombol diklik, SEMUA 3 Child di-render ulang (total 3 render untuk Child). Padahal props mereka nggak berubah!

Solusi dengan React.memo:

jsx
import { useState, memo } from 'react';

function Parent() {
  const [angka, setAngka] = useState(0);

  return (
    <div>
      <p>Angka: {angka}</p>
      <button onClick={() => setAngka(angka + 1)}>+1</button>
      <ChildMemo nama="Budi" />
      <ChildMemo nama="Ani" />
      <ChildMemo nama="Citra" />
    </div>
  );
}

// memo() = "jangan re-render kalau props sama"
const ChildMemo = memo(function Child({ nama }) {
  console.log(`${nama} di-render`);
  return <p>Halo, {nama}!</p>;
});

Sekarang saat tombol diklik, Child TIDAK di-render ulang karena props nama nggak berubah. React.memo membandingkan props lama vs baru, dan skip render kalau sama.

Catatan: jangan pakai memo di mana-mana. Cuma pakai kalau ada masalah performa yang terukur. Premature optimization is the root of all evil!


Ringkasan

  • React punya 3 tahap: TriggerRenderCommit
  • Trigger: initial render atau setState
  • Render: React memanggil komponen (fungsi) untuk menghitung JSX baru
  • Commit: React update DOM, cuma bagian yang berubah
  • React pakai Virtual DOM dan diffing untuk efisiensi
  • Batching: beberapa setState dalam satu handler = 1 render
  • Komponen harus pure saat render (nggak boleh ada side effect)
  • Child component ikut re-render saat parent re-render (default behavior)

Di bab selanjutnya, kita bakal bahas konsep penting: State sebagai Snapshot. Kenapa console.log setelah setState masih nunjukin nilai lama? Kenapa state "terkunci" dalam satu render? Jawabannya ada di bab berikutnya!

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.