Bab 8: Menjaga Komponen Tetap Pure

6 menit baca

Apa Itu "Pure"?

Sebelum masuk ke React, kita pahami dulu konsep "pure" (murni) di dunia nyata.

Bayangin mesin pembuat jus di pasar. Kamu masukin 3 buah jeruk, keluar jus jeruk. Besok kamu masukin 3 buah jeruk lagi, keluar jus jeruk yang persis sama. Mesin ini nggak tiba-tiba ngasih jus mangga padahal kamu masukin jeruk. Mesin ini juga nggak diam-diam nyimpen sisa jus dari pelanggan sebelumnya dan nyampurin ke jus kamu.

Mesin jus ini pure: input yang sama selalu menghasilkan output yang sama, dan nggak ada efek samping ke dunia luar.

Sekarang bayangin mesin jus yang "nggak pure": kadang ngasih jus jeruk, kadang jus campur, tergantung mood mesinnya atau tergantung siapa pelanggan sebelumnya. Kamu nggak bisa prediksi hasilnya. Serem kan?

Pure Function di JavaScript

Sebelum ke React, pahami dulu pure function di JavaScript biasa:

javascript
// ✅ PURE FUNCTION - input sama = output sama, tanpa efek samping
function tambah(a, b) {
  return a + b;
}
tambah(2, 3); // Selalu 5
tambah(2, 3); // Tetap 5, kapanpun dipanggil

function formatNama(depan, belakang) {
  return `${depan} ${belakang}`;
}
formatNama("Budi", "Santoso"); // Selalu "Budi Santoso"


// ❌ IMPURE FUNCTION - hasilnya bisa beda tiap dipanggil
let counter = 0;
function tambahCounter() {
  counter++;        // Mengubah variabel di luar fungsi (side effect!)
  return counter;
}
tambahCounter(); // 1
tambahCounter(); // 2 (beda! padahal nggak ada input)
tambahCounter(); // 3 (beda lagi!)

function waktuSekarang() {
  return new Date().toLocaleTimeString(); // Hasilnya beda tiap detik
}

Ciri-ciri Pure Function:

  1. Deterministic: Input yang sama SELALU menghasilkan output yang sama
  2. No side effects: Nggak mengubah apapun di luar fungsinya (nggak ubah variabel global, nggak nulis ke database, nggak ubah DOM langsung)
  3. Nggak bergantung pada state eksternal: Nggak baca variabel global yang bisa berubah

Komponen React Sebagai Pure Function

React menganggap setiap komponen sebagai pure function. Artinya:

Kalau props dan state yang sama diberikan ke komponen, komponen itu harus SELALU menghasilkan JSX yang sama.

jsx
// ✅ PURE COMPONENT - props sama = output sama
function Sapaan({ nama, jam }) {
  const waktu = jam < 12 ? "Pagi" : jam < 17 ? "Siang" : "Malam";
  return <h1>Selamat {waktu}, {nama}!</h1>;
}

// Kalau dipanggil dengan props yang sama, hasilnya SELALU sama:
<Sapaan nama="Budi" jam={9} />   // Selalu: "Selamat Pagi, Budi!"
<Sapaan nama="Budi" jam={9} />   // Tetap: "Selamat Pagi, Budi!"
jsx
// ❌ IMPURE COMPONENT - hasilnya bisa beda tiap render!
let panggilanKe = 0;

function SapaanBuruk({ nama }) {
  panggilanKe++;  // Mengubah variabel di luar komponen!
  return <h1>Halo {nama}! (render ke-{panggilanKe})</h1>;
}

// Render pertama: "Halo Budi! (render ke-1)"
// Render kedua:   "Halo Budi! (render ke-2)"  ← BEDA! Padahal props sama!

Kenapa React Butuh Komponen Pure?

Ini bukan cuma soal "best practice" atau "biar rapi." Ada alasan teknis yang kuat:

1. React Bisa Render Ulang Kapan Saja

React berhak me-render ulang komponen kamu kapanpun dia mau. Kalau komponen pure, nggak masalah. Hasilnya tetap sama. Tapi kalau impure, setiap re-render bisa menghasilkan hal berbeda dan bikin bug.

jsx
// ✅ Pure - aman di-render berapa kali pun
function Harga({ jumlah, hargaSatuan }) {
  return <p>Total: Rp {(jumlah * hargaSatuan).toLocaleString('id-ID')}</p>;
}
// Render 1x atau 100x, hasilnya tetap sama kalau props sama

// ❌ Impure - bahaya kalau di-render ulang
let totalGlobal = 0;
function HargaBahaya({ harga }) {
  totalGlobal += harga;  // Setiap render MENAMBAH total!
  return <p>Total: Rp {totalGlobal.toLocaleString('id-ID')}</p>;
}
// Render 1x: Total 15000
// Render 2x: Total 30000 (BUG!)
// Render 3x: Total 45000 (MAKIN PARAH!)

2. React Bisa Skip Render (Optimization)

Kalau React tahu komponen kamu pure dan props-nya nggak berubah, dia bisa skip rendering komponen itu (memoization). Ini bikin aplikasi lebih cepat. Tapi ini cuma aman kalau komponen benar-benar pure.

3. React Bisa Render di Server

Dengan Server Components dan SSR, komponen bisa di-render di server. Kalau komponen pure, hasilnya konsisten di mana pun di-render (server atau client). Kalau impure, bisa beda hasilnya.

4. StrictMode Double Render

Di development mode, React sengaja me-render komponen dua kali untuk mendeteksi impurity. Kalau komponen kamu pure, double render nggak masalah (hasilnya sama). Kalau impure, kamu bakal lihat bug.

jsx
// Di StrictMode, React panggil komponen 2x
// Pure component: render 1 = render 2 ✅
// Impure component: render 1 ≠ render 2 ❌ (bug terdeteksi!)

Side Effects: Musuh Purity

Side effect adalah apapun yang "menyentuh dunia luar" dari dalam komponen saat rendering. Contoh side effects:

jsx
// ❌ Side effects SAAT RENDERING (jangan dilakukan!)

// 1. Mengubah variabel di luar komponen
let hitungan = 0;
function Counter() {
  hitungan++;  // Side effect!
  return <p>{hitungan}</p>;
}

// 2. Mengubah DOM langsung
function Judul({ teks }) {
  document.title = teks;  // Side effect!
  return <h1>{teks}</h1>;
}

// 3. Mengubah objek/array yang sudah ada
function DaftarItem({ items }) {
  items.push("item baru");  // Side effect! Mutasi props!
  return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
}

// 4. Melakukan HTTP request
function DataUser({ userId }) {
  fetch(`/api/users/${userId}`);  // Side effect!
  return <p>Loading...</p>;
}

// 5. Menulis ke localStorage
function Tema({ mode }) {
  localStorage.setItem('tema', mode);  // Side effect!
  return <div className={mode}>...</div>;
}

Local Mutation: Ini Boleh!

Ada satu pengecualian penting: mutasi lokal (mengubah variabel/objek yang dibuat DI DALAM komponen saat render yang sama) itu boleh dan aman.

Analoginya: Kalau kamu masak di dapur sendiri, kamu boleh ngacak-ngacak dapur kamu. Yang nggak boleh itu ngacak-ngacak dapur tetangga. Selama "kekacauan" itu terjadi di dalam dan nggak bocor keluar, nggak masalah.

jsx
// ✅ Local mutation - AMAN!
function DaftarAngka() {
  // Variabel ini dibuat BARU setiap render
  const items = [];  // Array baru, milik render ini
  
  for (let i = 1; i <= 10; i++) {
    items.push(<li key={i}>Item {i}</li>);  // Mutasi array LOKAL - aman!
  }
  
  return <ul>{items}</ul>;
}
jsx
// ✅ Local mutation - AMAN!
function TabelPerkalian({ angka }) {
  // Objek baru dibuat setiap render
  const baris = [];
  
  for (let i = 1; i <= 10; i++) {
    baris.push(
      <tr key={i}>
        <td>{angka} × {i}</td>
        <td>=</td>
        <td>{angka * i}</td>
      </tr>
    );
  }
  
  return (
    <table>
      <tbody>{baris}</tbody>
    </table>
  );
}
jsx
// ✅ Membuat objek baru berdasarkan props - AMAN!
function ProfilLengkap({ user }) {
  // Bikin objek baru, nggak mengubah props
  const profilFormatted = {
    namaLengkap: `${user.depan} ${user.belakang}`,
    inisial: `${user.depan[0]}${user.belakang[0]}`,
    umur: new Date().getFullYear() - user.tahunLahir,
  };
  
  return (
    <div>
      <h2>{profilFormatted.namaLengkap}</h2>
      <span className="inisial">{profilFormatted.inisial}</span>
      <p>Umur: {profilFormatted.umur} tahun</p>
    </div>
  );
}

Bedakan: Local Mutation vs External Mutation

jsx
// ✅ LOCAL mutation - variabel dibuat di dalam, dimutasi di dalam
function Komponen() {
  const arr = [];           // Dibuat di sini
  arr.push("item");         // Dimutasi di sini - AMAN
  return <p>{arr.length}</p>;
}

// ❌ EXTERNAL mutation - variabel dari luar dimutasi di dalam
const globalArr = [];       // Dibuat di LUAR
function Komponen() {
  globalArr.push("item");   // Mutasi variabel luar - BAHAYA!
  return <p>{globalArr.length}</p>;
}

// ❌ PROPS mutation - props dimutasi
function Komponen({ items }) {
  items.sort();             // Mutasi props - BAHAYA!
  return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
}

// ✅ COPY then mutate - bikin copy, mutasi copy-nya
function Komponen({ items }) {
  const sorted = [...items].sort();  // Copy baru, mutasi copy - AMAN
  return <ul>{sorted.map(i => <li key={i}>{i}</li>)}</ul>;
}

Di Mana Side Effects Boleh Dilakukan?

Side effects itu bukan hal jahat. Aplikasi BUTUH side effects (fetch data, update DOM, simpan ke storage). Yang penting: jangan lakukan saat rendering. Lakukan di tempat yang tepat:

1. Event Handlers (Paling Umum)

jsx
function FormKontak() {
  function handleSubmit(e) {
    e.preventDefault();
    // ✅ Side effect di event handler - AMAN!
    fetch('/api/kontak', {
      method: 'POST',
      body: JSON.stringify({ nama: 'Budi', pesan: 'Halo' })
    });
    alert('Pesan terkirim!');  // Side effect - aman di sini
  }
  
  function handleKlik() {
    // ✅ Side effect di event handler - AMAN!
    console.log('Tombol diklik');
    document.title = 'Diklik!';
    localStorage.setItem('lastClick', Date.now());
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <button onClick={handleKlik}>Kirim</button>
    </form>
  );
}

Event handler nggak dijalankan saat rendering. Mereka dijalankan sebagai respons terhadap aksi user (klik, submit, dll). Jadi side effects di sini aman.

2. useEffect (Untuk Side Effects yang Perlu Sinkronisasi)

jsx
import { useEffect } from 'react';

function HalamanProduk({ produkId }) {
  const [produk, setProduk] = useState(null);
  
  // ✅ Side effect di useEffect - dijalankan SETELAH render
  useEffect(() => {
    // Fetch data setelah komponen di-render
    fetch(`/api/produk/${produkId}`)
      .then(res => res.json())
      .then(data => setProduk(data));
  }, [produkId]);
  
  // ✅ Side effect di useEffect - update document title
  useEffect(() => {
    document.title = produk ? produk.nama : 'Loading...';
  }, [produk]);
  
  // Render tetap pure - cuma baca state dan props
  if (!produk) return <p>Memuat...</p>;
  return <h1>{produk.nama}</h1>;
}

useEffect menjalankan kode setelah render selesai. Jadi rendering tetap pure, dan side effects terjadi terpisah.

Catatan: useEffect dibahas detail di Part 3. Untuk sekarang, cukup tahu bahwa ini tempat yang tepat untuk side effects yang perlu terjadi setelah render.

3. Kapan Pakai Event Handler vs useEffect?

SituasiPakai
User klik tombol → fetch dataEvent handler
User submit form → kirim ke serverEvent handler
Komponen muncul → fetch data awaluseEffect
Props berubah → update document titleuseEffect
Props berubah → sinkronisasi dengan external systemuseEffect

Aturan praktis: Kalau side effect dipicu oleh aksi user, pakai event handler. Kalau dipicu oleh rendering (komponen muncul atau props/state berubah), pakai useEffect.

StrictMode: Detektif Impurity

React punya fitur bernama StrictMode yang membantu mendeteksi komponen impure. Di development mode, StrictMode me-render setiap komponen dua kali.

jsx
// Di index.js atau main.jsx:
import { StrictMode } from 'react';

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

Kenapa Double Render Membantu?

jsx
// Komponen impure - ketahuan di StrictMode!
let idCounter = 0;

function ItemBaru() {
  idCounter++;  // Side effect!
  return <p>Item #{idCounter}</p>;
}

// Tanpa StrictMode:
// Render 1: "Item #1" ← keliatan bener

// Dengan StrictMode (double render):
// Render 1: "Item #1"
// Render 2: "Item #2" ← BEDA! Bug terdeteksi!
jsx
// Komponen pure - aman di StrictMode
function ItemBaru({ nomor }) {
  return <p>Item #{nomor}</p>;
}

// Dengan StrictMode (double render):
// Render 1: "Item #5"
// Render 2: "Item #5" ← SAMA! Aman ✅

Penting: StrictMode cuma aktif di development. Di production, komponen di-render sekali seperti biasa. Jadi nggak ada performance impact di production.

Contoh Nyata: Refactoring Impure ke Pure

Sebelum (Impure):

jsx
// ❌ IMPURE - banyak masalah
let guestCount = 0;

function NomorTamu() {
  guestCount++;  // Mutasi variabel luar
  
  // Langsung manipulasi DOM
  document.getElementById('counter').innerText = guestCount;
  
  return (
    <div>
      <h2>Tamu #{guestCount}</h2>
      <p>Waktu: {new Date().toLocaleTimeString()}</p>
    </div>
  );
}

Sesudah (Pure):

jsx
// ✅ PURE - bersih dan predictable
function NomorTamu({ nomor }) {
  // Semua data dari props - nggak ada variabel luar
  return (
    <div>
      <h2>Tamu #{nomor}</h2>
    </div>
  );
}

// Side effects dipindah ke tempat yang tepat
function DaftarTamu() {
  const [jumlahTamu, setJumlahTamu] = useState(0);
  
  // Side effect di useEffect
  useEffect(() => {
    document.title = `${jumlahTamu} tamu terdaftar`;
  }, [jumlahTamu]);
  
  // Event handler untuk aksi user
  function handleTambahTamu() {
    setJumlahTamu(jumlahTamu + 1);
  }
  
  return (
    <div>
      <button onClick={handleTambahTamu}>Tambah Tamu</button>
      {Array.from({ length: jumlahTamu }, (_, i) => (
        <NomorTamu key={i + 1} nomor={i + 1} />
      ))}
    </div>
  );
}

Contoh Lengkap: Komponen Pure vs Impure

jsx
// ===== VERSI IMPURE (JANGAN DITIRU!) =====

let pesananGlobal = [];  // Variabel global - bahaya!

function FormPesananBuruk({ menu }) {
  // ❌ Mutasi variabel global saat render
  pesananGlobal.push(menu);
  
  // ❌ Langsung ubah DOM
  document.title = `${pesananGlobal.length} pesanan`;
  
  // ❌ Baca waktu saat render (nggak deterministic)
  const waktuPesan = new Date().toLocaleTimeString();
  
  return (
    <div>
      <p>Pesanan: {menu}</p>
      <p>Waktu: {waktuPesan}</p>
      <p>Total pesanan: {pesananGlobal.length}</p>
    </div>
  );
}


// ===== VERSI PURE (CONTOH YANG BENAR) =====

function ItemPesanan({ menu, nomor, waktu }) {
  // ✅ Semua data dari props
  // ✅ Nggak ada side effect
  // ✅ Output selalu sama untuk input yang sama
  return (
    <div className="item-pesanan">
      <span className="nomor">#{nomor}</span>
      <span className="menu">{menu}</span>
      <span className="waktu">{waktu}</span>
    </div>
  );
}

function DaftarPesanan() {
  const [pesanan, setPesanan] = useState([]);
  
  // ✅ Side effect di useEffect
  useEffect(() => {
    document.title = `${pesanan.length} pesanan`;
  }, [pesanan]);
  
  // ✅ Side effect di event handler
  function handleTambahPesanan(menu) {
    const pesananBaru = {
      id: Date.now(),
      menu: menu,
      waktu: new Date().toLocaleTimeString(),
    };
    setPesanan([...pesanan, pesananBaru]);  // State update, bukan mutasi
  }
  
  return (
    <div>
      <h2>Daftar Pesanan ({pesanan.length})</h2>
      
      <div className="tombol-menu">
        <button onClick={() => handleTambahPesanan("Nasi Goreng")}>
          + Nasi Goreng
        </button>
        <button onClick={() => handleTambahPesanan("Mie Ayam")}>
          + Mie Ayam
        </button>
        <button onClick={() => handleTambahPesanan("Es Teh")}>
          + Es Teh
        </button>
      </div>
      
      {/* Komponen pure - cuma terima props dan render */}
      {pesanan.map((p, index) => (
        <ItemPesanan 
          key={p.id}
          menu={p.menu}
          nomor={index + 1}
          waktu={p.waktu}
        />
      ))}
    </div>
  );
}

Checklist: Apakah Komponen Saya Pure?

Tanya diri sendiri:

  1. ✅ Apakah komponen ini menghasilkan output yang sama kalau props/state sama?
  2. ✅ Apakah komponen ini TIDAK mengubah variabel di luar dirinya?
  3. ✅ Apakah komponen ini TIDAK mengubah props yang diterima?
  4. ✅ Apakah komponen ini TIDAK melakukan fetch/DOM manipulation/localStorage saat render?
  5. ✅ Apakah semua "kekacauan" (mutasi) terjadi pada variabel yang dibuat di dalam komponen?

Kalau semua jawaban "ya", komponen kamu pure! 🎉

⚠️ Jebakan

Jebakan 1: Mutasi Props

jsx
// ❌ Mengubah props - IMPURE!
function Daftar({ items }) {
  items.push("item baru");  // Mutasi array dari parent!
  items.sort();              // Mutasi lagi!
  return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
}

// ✅ Bikin copy baru
function Daftar({ items }) {
  const sorted = [...items, "item baru"].sort();
  return <ul>{sorted.map(i => <li key={i}>{i}</li>)}</ul>;
}

Jebakan 2: Variabel di Luar Komponen

jsx
// ❌ Variabel di luar yang dimutasi saat render
let renderCount = 0;

function App() {
  renderCount++;  // Berubah setiap render!
  return <p>Render ke-{renderCount}</p>;
}

// ✅ Pakai state kalau butuh counter
function App() {
  // useRef untuk counter yang nggak trigger re-render
  const renderCount = useRef(0);
  renderCount.current++;
  
  // Atau useEffect untuk logging
  useEffect(() => {
    console.log('Component rendered');
  });
  
  return <p>App</p>;
}

Jebakan 3: Membaca Waktu/Random Saat Render

jsx
// ❌ Nggak deterministic - hasilnya beda tiap render
function Komponen() {
  const sekarang = new Date();  // Beda tiap render!
  const random = Math.random(); // Beda tiap render!
  return <p>{sekarang.toString()} - {random}</p>;
}

// ✅ Terima sebagai props atau hitung di event handler
function Komponen({ waktuDibuat }) {
  return <p>Dibuat: {waktuDibuat}</p>;
}

// Atau pakai state yang di-set sekali
function Komponen() {
  const [waktu] = useState(() => new Date().toLocaleTimeString());
  return <p>Dibuat: {waktu}</p>;
}

Jebakan 4: Mengubah DOM Langsung

jsx
// ❌ Manipulasi DOM saat render
function Judul({ teks }) {
  document.title = teks;  // Side effect saat render!
  document.body.style.backgroundColor = 'blue';  // Side effect!
  return <h1>{teks}</h1>;
}

// ✅ Pakai useEffect
function Judul({ teks }) {
  useEffect(() => {
    document.title = teks;
  }, [teks]);
  
  return <h1>{teks}</h1>;
}

Jebakan 5: Bingung "Kapan Boleh Mutasi"

jsx
// Aturan simpel:
// - Dibuat DI DALAM komponen, SAAT render yang sama → BOLEH dimutasi
// - Dibuat DI LUAR atau diterima sebagai PROPS → JANGAN dimutasi

function Komponen({ dataDariLuar }) {
  // ✅ Dibuat di dalam - boleh dimutasi
  const arrLokal = [];
  arrLokal.push("item");  // Aman!
  
  const objLokal = {};
  objLokal.nama = "Budi";  // Aman!
  
  // ❌ Dari luar - jangan dimutasi
  dataDariLuar.push("item");  // Bahaya!
  dataDariLuar.nama = "Budi"; // Bahaya!
  
  // ✅ Kalau perlu "ubah" data dari luar, bikin copy
  const copyData = [...dataDariLuar, "item"];  // Aman!
  const copyObj = { ...dataDariLuar, nama: "Budi" };  // Aman!
  
  return <div>...</div>;
}

Ringkasan

KonsepPenjelasan
Pure componentInput sama = output sama, tanpa side effect
Side effectApapun yang "menyentuh dunia luar" saat render
Local mutationMutasi variabel yang dibuat di dalam komponen (AMAN)
Event handlerTempat yang tepat untuk side effects dari aksi user
useEffectTempat untuk side effects yang perlu sinkronisasi
StrictModeDouble render untuk deteksi impurity
Jangan mutasi propsSelalu bikin copy kalau perlu "ubah" data dari luar

🏋️ Challenge

Challenge 1: Identifikasi Impurity

Perhatikan komponen berikut. Identifikasi semua masalah purity-nya dan perbaiki:

jsx
let totalPesanan = 0;
const riwayat = [];

function KartuPesanan({ nama, harga }) {
  totalPesanan += harga;
  riwayat.push({ nama, harga, waktu: new Date() });
  document.title = `Total: Rp ${totalPesanan}`;
  
  return (
    <div>
      <h3>{nama}</h3>
      <p>Harga: Rp {harga.toLocaleString('id-ID')}</p>
      <p>Total semua: Rp {totalPesanan.toLocaleString('id-ID')}</p>
      <p>Pesanan ke-{riwayat.length}</p>
    </div>
  );
}
💡 Hint

Masalah:

  1. Mutasi totalPesanan (variabel luar)
  2. Mutasi riwayat (variabel luar)
  3. document.title diubah saat render
  4. new Date() bikin output nggak deterministic

Solusi: pindahkan state ke dalam komponen parent, pakai useEffect untuk document.title

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

// ✅ Komponen pure - cuma terima props dan render
function KartuPesanan({ nama, harga, nomor }) {
  return (
    <div>
      <h3>{nama}</h3>
      <p>Harga: Rp {harga.toLocaleString('id-ID')}</p>
      <p>Pesanan ke-{nomor}</p>
    </div>
  );
}

// ✅ State dan side effects dikelola di parent
function DaftarPesanan() {
  const [pesanan, setPesanan] = useState([]);
  
  // Total dihitung dari state (derived state)
  const totalPesanan = pesanan.reduce((sum, p) => sum + p.harga, 0);
  
  // Side effect di useEffect
  useEffect(() => {
    document.title = `Total: Rp ${totalPesanan.toLocaleString('id-ID')}`;
  }, [totalPesanan]);
  
  // Side effect di event handler
  function handleTambahPesanan(nama, harga) {
    setPesanan([...pesanan, { 
      id: Date.now(), 
      nama, 
      harga, 
      waktu: new Date() 
    }]);
  }
  
  return (
    <div>
      <h2>Total: Rp {totalPesanan.toLocaleString('id-ID')}</h2>
      
      <button onClick={() => handleTambahPesanan("Nasi Goreng", 15000)}>
        + Nasi Goreng
      </button>
      <button onClick={() => handleTambahPesanan("Es Teh", 5000)}>
        + Es Teh
      </button>
      
      {pesanan.map((p, index) => (
        <KartuPesanan 
          key={p.id}
          nama={p.nama}
          harga={p.harga}
          nomor={index + 1}
        />
      ))}
    </div>
  );
}

Challenge 2: Pure Formatting Component

Buat komponen FormatUang yang:

  • Menerima props: jumlah (number), mata_uang ("IDR", "USD", "EUR"), tampilSimbol (boolean)
  • Me-return string yang terformat dengan benar
  • HARUS pure: input sama = output sama, tanpa side effect
  • Handle edge cases: jumlah negatif, jumlah 0, mata uang nggak dikenal
💡 Hint
  • Bikin object mapping untuk simbol dan format
  • Semua logika bisa dilakukan tanpa side effect
  • Pakai Math.abs() untuk handle negatif
  • Default value untuk mata uang nggak dikenal
✅ Solusi
jsx
// ✅ 100% Pure - nggak ada side effect, deterministic
function FormatUang({ jumlah, mata_uang = "IDR", tampilSimbol = true }) {
  // Config per mata uang (objek lokal, dibuat tiap render - aman)
  const config = {
    IDR: { simbol: 'Rp', locale: 'id-ID', desimal: 0 },
    USD: { simbol: '$', locale: 'en-US', desimal: 2 },
    EUR: { simbol: '€', locale: 'de-DE', desimal: 2 },
  };
  
  // Fallback untuk mata uang nggak dikenal
  const cfg = config[mata_uang] || { simbol: mata_uang, locale: 'en-US', desimal: 2 };
  
  // Handle negatif
  const isNegatif = jumlah < 0;
  const nilaiAbsolut = Math.abs(jumlah);
  
  // Format angka
  const formatted = nilaiAbsolut.toLocaleString(cfg.locale, {
    minimumFractionDigits: cfg.desimal,
    maximumFractionDigits: cfg.desimal,
  });
  
  // Susun output
  const simbol = tampilSimbol ? cfg.simbol + ' ' : '';
  const tanda = isNegatif ? '-' : '';
  
  // Style berdasarkan nilai
  const warna = isNegatif ? 'red' : jumlah === 0 ? '#999' : 'inherit';
  
  return (
    <span style={{ color: warna, fontVariantNumeric: 'tabular-nums' }}>
      {tanda}{simbol}{formatted}
    </span>
  );
}

// Penggunaan - hasilnya SELALU sama untuk input yang sama:
function ContohPenggunaan() {
  return (
    <div>
      <p>Saldo: <FormatUang jumlah={1500000} mata_uang="IDR" /></p>
      <p>Harga: <FormatUang jumlah={29.99} mata_uang="USD" /></p>
      <p>Rugi: <FormatUang jumlah={-50000} mata_uang="IDR" /></p>
      <p>Kosong: <FormatUang jumlah={0} mata_uang="EUR" /></p>
      <p>Unknown: <FormatUang jumlah={100} mata_uang="BTC" /></p>
      <p>Tanpa simbol: <FormatUang jumlah={75000} tampilSimbol={false} /></p>
    </div>
  );
}

Challenge 3: Refactor Impure Component

Refactor komponen berikut agar pure. Komponen ini menampilkan daftar tugas dan punya beberapa masalah purity:

jsx
let nextId = 1;
const allTasks = [];

function TaskManager({ initialTasks }) {
  // Mutasi array luar
  initialTasks.forEach(task => {
    task.id = nextId++;
    allTasks.push(task);
  });
  
  // Sort mutasi array
  allTasks.sort((a, b) => a.priority - b.priority);
  
  // Side effect
  console.log('Rendered with', allTasks.length, 'tasks');
  localStorage.setItem('taskCount', allTasks.length);
  
  return (
    <div>
      <h2>Tasks ({allTasks.length})</h2>
      {allTasks.map(task => (
        <div key={task.id}>
          <span>{task.nama}</span>
          <span>Priority: {task.priority}</span>
        </div>
      ))}
    </div>
  );
}
💡 Hint

Masalah:

  1. nextId dan allTasks adalah variabel luar yang dimutasi
  2. initialTasks.forEach memutasi props (menambah .id)
  3. allTasks.sort() mutasi array
  4. console.log dan localStorage adalah side effects saat render

Solusi: semua data jadi state/props, side effects ke useEffect/event handler

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

// ✅ Komponen item - pure
function TaskItem({ nama, priority }) {
  return (
    <div style={{ 
      padding: '8px', 
      borderLeft: `3px solid ${priority <= 2 ? 'red' : priority <= 4 ? 'orange' : 'green'}`,
      marginBottom: '4px'
    }}>
      <span>{nama}</span>
      <span style={{ marginLeft: '12px', color: '#666' }}>
        Priority: {priority}
      </span>
    </div>
  );
}

// ✅ Komponen manager - state dikelola dengan benar
function TaskManager({ initialTasks }) {
  // State internal - bukan variabel global
  const [tasks] = useState(() => {
    // Inisialisasi state: bikin copy dengan ID, TANPA mutasi props
    return initialTasks.map((task, index) => ({
      ...task,           // Copy properti, nggak mutasi original
      id: index + 1,     // Tambah ID di copy baru
    }));
  });
  
  // Derived data: sort tanpa mutasi (bikin copy baru)
  const sortedTasks = [...tasks].sort((a, b) => a.priority - b.priority);
  
  // Side effects di useEffect
  useEffect(() => {
    console.log('Rendered with', tasks.length, 'tasks');
    localStorage.setItem('taskCount', tasks.length);
  }, [tasks.length]);
  
  return (
    <div>
      <h2>Tasks ({sortedTasks.length})</h2>
      {sortedTasks.map(task => (
        <TaskItem 
          key={task.id}
          nama={task.nama}
          priority={task.priority}
        />
      ))}
    </div>
  );
}

// Penggunaan:
function App() {
  const tugasAwal = [
    { nama: "Deploy ke production", priority: 1 },
    { nama: "Fix bug login", priority: 2 },
    { nama: "Update dokumentasi", priority: 5 },
    { nama: "Review PR", priority: 3 },
  ];
  
  return <TaskManager initialTasks={tugasAwal} />;
}

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.