Bab 2: Thinking in React — Cara Berpikir ala React

5 menit baca

Kenapa Bab Ini Penting?

Banyak pemula langsung nulis kode React tanpa berpikir dulu. Hasilnya? Kode berantakan, state di mana-mana, component nggak jelas tanggung jawabnya.

React punya "cara berpikir" sendiri. Kalau kamu ikuti, kode kamu bakal rapi, mudah di-maintain, dan jarang bug. Bab ini ngajarin 5 langkah yang selalu dipakai developer React profesional sebelum nulis kode.

💡Info
  • Tukang bangunan langsung pasang bata tanpa gambar → rumah miring, kamar kegedean, pintu nabrak tembok.
  • Arsitek gambar blueprint dulu → ukur, rencanain, baru bangun → rumah rapi.

Thinking in React = jadi arsitek sebelum jadi tukang bangunan.


Studi Kasus: Tabel Produk dengan Filter

Kita akan bangun UI ini:

┌─────────────────────────────────┐ │ [🔍 Cari produk... ] │ │ [✓] Hanya tampilkan stok ada │ │ │ │ Buah-buahan │ │ Apel Rp 10.000 │ │ Mangga Rp 15.000 │ │ Durian Rp 50.000 │ ← merah (stok habis) │ │ │ Sayuran │ │ Bayam Rp 5.000 │ │ Wortel Rp 8.000 │ │ Brokoli Rp 12.000 │ ← merah (stok habis) └─────────────────────────────────┘

Data-nya:

js
const produk = [
  { kategori: "Buah-buahan", harga: "Rp 10.000", stok: true, nama: "Apel" },
  { kategori: "Buah-buahan", harga: "Rp 15.000", stok: true, nama: "Mangga" },
  { kategori: "Buah-buahan", harga: "Rp 50.000", stok: false, nama: "Durian" },
  { kategori: "Sayuran", harga: "Rp 5.000", stok: true, nama: "Bayam" },
  { kategori: "Sayuran", harga: "Rp 8.000", stok: true, nama: "Wortel" },
  { kategori: "Sayuran", harga: "Rp 12.000", stok: false, nama: "Brokoli" },
];

Sekarang kita ikuti 5 langkah Thinking in React.


Langkah 1: Pecah UI Jadi Hierarki Component

Lihat mockup di atas, lalu gambar kotak di sekeliling setiap bagian yang bisa jadi component sendiri.

Prinsip: Satu component = satu tanggung jawab.

Hasilnya:

FilterableProdukTable (seluruh app) ├── SearchBar (input + checkbox) └── TabelProduk (tabel data) ├── KategoriRow (header kategori: "Buah-buahan") └── ProdukRow (satu baris produk)

Cara Menentukan "Ini Harus Jadi Component Sendiri Nggak?"

Tanya diri sendiri:

  1. Apakah bagian ini punya tanggung jawab sendiri? → Kalau ya, jadikan component.
  2. Apakah bagian ini bisa di-reuse? → Kalau ya, jadikan component.
  3. Apakah bagian ini terlalu kompleks kalau digabung? → Kalau ya, pecah.
💡Info
  • FilterableProdukTable = CEO (mengatur semuanya)
  • SearchBar = Divisi Marketing (terima input dari customer)
  • TabelProduk = Divisi Produksi (tampilkan produk)
  • KategoriRow = Kepala Bagian (header per kategori)
  • ProdukRow = Staff (satu produk)

Setiap orang punya job desc jelas. Nggak ada yang ngerjain semuanya sendiri.


Langkah 2: Bangun Versi Statis (Tanpa Interaksi)

Sekarang kita koding — tapi belum ada interaksi. Cuma tampilkan data.

jsx
function ProdukRow({ produk }) {
  const nama = produk.stok ? produk.nama : (
    <span style={{ color: "red" }}>{produk.nama}</span>
  );

  return (
    <tr>
      <td>{nama}</td>
      <td>{produk.harga}</td>
    </tr>
  );
}

function KategoriRow({ kategori }) {
  return (
    <tr>
      <th colSpan="2">{kategori}</th>
    </tr>
  );
}

function TabelProduk({ produk }) {
  const rows = [];
  let kategoriTerakhir = null;

  produk.forEach((item) => {
    if (item.kategori !== kategoriTerakhir) {
      rows.push(<KategoriRow kategori={item.kategori} key={item.kategori} />);
    }
    rows.push(<ProdukRow produk={item} key={item.nama} />);
    kategoriTerakhir = item.kategori;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Nama</th>
          <th>Harga</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

function SearchBar() {
  return (
    <form>
      <input type="text" placeholder="Cari produk..." />
      <label>
        <input type="checkbox" />
        {" "}Hanya tampilkan stok ada
      </label>
    </form>
  );
}

function FilterableProdukTable({ produk }) {
  return (
    <div>
      <SearchBar />
      <TabelProduk produk={produk} />
    </div>
  );
}

Kenapa Statis Dulu?

  • Banyak ketik, sedikit mikir — kamu cuma perlu fokus ke struktur
  • Nggak perlu state — data mengalir satu arah lewat props
  • Lebih mudah debug — kalau tampilan salah, pasti masalah di props/render

Langkah 3: Tentukan State Minimal

Sekarang kita mau tambah interaksi. Pertanyaannya: data apa yang perlu jadi state?

Semua data di app kita:

  1. Daftar produk asli
  2. Teks yang diketik user di search
  3. Nilai checkbox (centang/tidak)
  4. Daftar produk yang sudah difilter

Tes: Apakah Ini State?

Untuk setiap data, tanya 3 pertanyaan:

  • Apakah nggak berubah seiring waktu? → Bukan state
  • Apakah dikirim dari parent lewat props? → Bukan state
  • Apakah bisa dihitung dari state/props lain? → Bukan state
DataState?Alasan
Daftar produk asliDikirim lewat props
Teks searchBerubah saat user ketik, nggak bisa dihitung
Nilai checkboxBerubah saat user centang, nggak bisa dihitung
Daftar produk terfilterBisa dihitung dari produk + search + checkbox

Kesimpulan: Cuma butuh 2 state — teksFilter dan hanyaStok.

💡Info
  • State = baju yang kamu BELI (koleksi asli)
  • Bukan state = baju yang kamu LIHAT di lemari setelah filter "warna biru" (itu cuma hasil filter dari koleksi asli)

Kamu nggak perlu simpan "daftar baju biru" terpisah. Cukup simpan semua baju + filter yang aktif, lalu hitung hasilnya.


Langkah 4: Tentukan Di Mana State Tinggal

State harus tinggal di component yang paling masuk akal. Caranya:

  1. Cari semua component yang butuh state itu
  2. Cari parent terdekat yang mencakup semuanya
  3. Taruh state di situ

Untuk kasus kita:

  • SearchBar butuh teksFilter dan hanyaStok (untuk tampilkan di input)
  • TabelProduk butuh teksFilter dan hanyaStok (untuk filter data)
  • Parent terdekat keduanya = FilterableProdukTable

Jadi state tinggal di FilterableProdukTable!

jsx
import { useState } from 'react';

function FilterableProdukTable({ produk }) {
  const [teksFilter, setTeksFilter] = useState("");
  const [hanyaStok, setHanyaStok] = useState(false);

  return (
    <div>
      <SearchBar
        teksFilter={teksFilter}
        hanyaStok={hanyaStok}
      />
      <TabelProduk
        produk={produk}
        teksFilter={teksFilter}
        hanyaStok={hanyaStok}
      />
    </div>
  );
}
💡Info

State itu kayak papan pengumuman di kantor:

  • Taruh di lantai yang bisa dilihat semua divisi yang butuh info itu
  • Kalau cuma divisi A yang butuh → taruh di lantai divisi A
  • Kalau divisi A dan B butuh → taruh di lantai bos mereka (parent)

Langkah 5: Tambahkan Aliran Data Terbalik (Inverse Data Flow)

Sekarang data mengalir ke bawah (parent → child lewat props). Tapi user mengetik di SearchBar — bagaimana SearchBar bisa update state di parent?

Jawabannya: Parent kirim fungsi setState sebagai props ke child.

jsx
function FilterableProdukTable({ produk }) {
  const [teksFilter, setTeksFilter] = useState("");
  const [hanyaStok, setHanyaStok] = useState(false);

  return (
    <div>
      <SearchBar
        teksFilter={teksFilter}
        hanyaStok={hanyaStok}
        onTeksFilterChange={setTeksFilter}
        onHanyaStokChange={setHanyaStok}
      />
      <TabelProduk
        produk={produk}
        teksFilter={teksFilter}
        hanyaStok={hanyaStok}
      />
    </div>
  );
}

function SearchBar({ teksFilter, hanyaStok, onTeksFilterChange, onHanyaStokChange }) {
  return (
    <form>
      <input
        type="text"
        value={teksFilter}
        placeholder="Cari produk..."
        onChange={(e) => onTeksFilterChange(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={hanyaStok}
          onChange={(e) => onHanyaStokChange(e.target.checked)}
        />
        {" "}Hanya tampilkan stok ada
      </label>
    </form>
  );
}

Alur Data Lengkap

User ketik "apel" di SearchBar ↓ onChange dipanggil → onTeksFilterChange("apel") ↓ setTeksFilter("apel") di parent ↓ State berubah → React re-render FilterableProdukTable ↓ Props baru dikirim ke SearchBar (input menampilkan "apel") ↓ Props baru dikirim ke TabelProduk (filter produk yang mengandung "apel")
💡Info
  • Parent = manajer yang pegang remote AC
  • Child (SearchBar) = resepsionis yang terima request dari tamu
  • Aliran data terbalik = resepsionis telepon manajer: "Pak, tamu minta AC dinginkan!" → manajer pencet remote → AC berubah → semua ruangan terasa efeknya

Kode Lengkap: Tabel Produk dengan Filter

jsx
import { useState } from 'react';

// Data
const PRODUK = [
  { kategori: "Buah-buahan", harga: "Rp 10.000", stok: true, nama: "Apel" },
  { kategori: "Buah-buahan", harga: "Rp 15.000", stok: true, nama: "Mangga" },
  { kategori: "Buah-buahan", harga: "Rp 50.000", stok: false, nama: "Durian" },
  { kategori: "Sayuran", harga: "Rp 5.000", stok: true, nama: "Bayam" },
  { kategori: "Sayuran", harga: "Rp 8.000", stok: true, nama: "Wortel" },
  { kategori: "Sayuran", harga: "Rp 12.000", stok: false, nama: "Brokoli" },
];

// Component: Satu baris produk
function ProdukRow({ produk }) {
  const nama = produk.stok
    ? produk.nama
    : <span style={{ color: "red" }}>{produk.nama}</span>;

  return (
    <tr>
      <td>{nama}</td>
      <td>{produk.harga}</td>
    </tr>
  );
}

// Component: Header kategori
function KategoriRow({ kategori }) {
  return (
    <tr>
      <th colSpan="2" style={{ textAlign: "left", paddingTop: "10px" }}>
        {kategori}
      </th>
    </tr>
  );
}

// Component: Tabel produk (dengan filter)
function TabelProduk({ produk, teksFilter, hanyaStok }) {
  const rows = [];
  let kategoriTerakhir = null;

  produk.forEach((item) => {
    // Filter berdasarkan teks search
    if (item.nama.toLowerCase().indexOf(teksFilter.toLowerCase()) === -1) {
      return;
    }
    // Filter berdasarkan checkbox stok
    if (hanyaStok && !item.stok) {
      return;
    }

    if (item.kategori !== kategoriTerakhir) {
      rows.push(<KategoriRow kategori={item.kategori} key={item.kategori} />);
    }
    rows.push(<ProdukRow produk={item} key={item.nama} />);
    kategoriTerakhir = item.kategori;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Nama</th>
          <th>Harga</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

// Component: Search bar
function SearchBar({ teksFilter, hanyaStok, onTeksFilterChange, onHanyaStokChange }) {
  return (
    <form>
      <input
        type="text"
        value={teksFilter}
        placeholder="Cari produk..."
        onChange={(e) => onTeksFilterChange(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={hanyaStok}
          onChange={(e) => onHanyaStokChange(e.target.checked)}
        />
        {" "}Hanya tampilkan stok ada
      </label>
    </form>
  );
}

// Component: App utama
function FilterableProdukTable({ produk }) {
  const [teksFilter, setTeksFilter] = useState("");
  const [hanyaStok, setHanyaStok] = useState(false);

  return (
    <div>
      <SearchBar
        teksFilter={teksFilter}
        hanyaStok={hanyaStok}
        onTeksFilterChange={setTeksFilter}
        onHanyaStokChange={setHanyaStok}
      />
      <TabelProduk
        produk={produk}
        teksFilter={teksFilter}
        hanyaStok={hanyaStok}
      />
    </div>
  );
}

// Render
export default function App() {
  return <FilterableProdukTable produk={PRODUK} />;
}

Ringkasan 5 Langkah

LangkahAksiHasil
1Pecah UI jadi componentHierarki component
2Bangun versi statisUI tanpa interaksi
3Tentukan state minimalDaftar state yang dibutuhkan
4Tentukan lokasi stateState di component yang tepat
5Tambah inverse data flowApp interaktif penuh

⚠️ Jebakan

1. Terlalu banyak state

jsx
// ❌ State berlebihan — produkFiltered bisa DIHITUNG
const [produk, setProduk] = useState(data);
const [filter, setFilter] = useState("");
const [produkFiltered, setProdukFiltered] = useState(data); // REDUNDAN!

// ✅ Cukup hitung saat render
const [produk, setProduk] = useState(data);
const [filter, setFilter] = useState("");
const produkFiltered = produk.filter(p => p.nama.includes(filter)); // Dihitung!

2. State di tempat yang salah

Kalau 2 component butuh state yang sama, jangan duplikasi — angkat ke parent!

3. Component terlalu besar

Kalau satu component lebih dari 100 baris, kemungkinan besar bisa dipecah jadi beberapa component kecil.


🏋️ Challenge

Buat app yang:

  • Punya array kontak (nama + nomor telepon)
  • Ada input search untuk filter kontak berdasarkan nama
  • Tampilkan kontak yang cocok

Ikuti 5 langkah Thinking in React:

  1. Pecah jadi component (SearchInput, KontakList, KontakItem)
  2. Bangun statis dulu
  3. State minimal: teks search
  4. State di parent
  5. Inverse data flow
💡 Hint

Component hierarchy:

App ├── SearchInput (terima onSearch prop) └── KontakList (terima kontak yang sudah difilter) └── KontakItem (satu kontak)

Filter di parent: kontak.filter(k => k.nama.toLowerCase().includes(search.toLowerCase()))

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

const KONTAK = [
  { id: 1, nama: "Budi Santoso", telp: "0812-1234-5678" },
  { id: 2, nama: "Ani Wijaya", telp: "0813-9876-5432" },
  { id: 3, nama: "Citra Dewi", telp: "0856-1111-2222" },
  { id: 4, nama: "Doni Pratama", telp: "0878-3333-4444" },
  { id: 5, nama: "Eka Putri", telp: "0821-5555-6666" },
];

function KontakItem({ kontak }) {
  return (
    <div style={{ padding: "8px", borderBottom: "1px solid #eee" }}>
      <strong>{kontak.nama}</strong>
      <p style={{ margin: 0, color: "#666" }}>{kontak.telp}</p>
    </div>
  );
}

function KontakList({ kontak }) {
  if (kontak.length === 0) {
    return <p>Tidak ada kontak yang cocok.</p>;
  }
  return (
    <div>
      {kontak.map(k => <KontakItem key={k.id} kontak={k} />)}
    </div>
  );
}

function SearchInput({ value, onChange }) {
  return (
    <input
      type="text"
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder="Cari kontak..."
      style={{ width: "100%", padding: "8px", marginBottom: "12px" }}
    />
  );
}

export default function App() {
  const [search, setSearch] = useState("");

  const kontakFiltered = KONTAK.filter(k =>
    k.nama.toLowerCase().includes(search.toLowerCase())
  );

  return (
    <div style={{ maxWidth: "400px", margin: "20px auto" }}>
      <h2>Kontak Saya</h2>
      <SearchInput value={search} onChange={setSearch} />
      <KontakList kontak={kontakFiltered} />
      <p style={{ color: "#999" }}>
        Menampilkan {kontakFiltered.length} dari {KONTAK.length} kontak
      </p>
    </div>
  );
}

Challenge 2: Keranjang Belanja

Buat app keranjang belanja sederhana:

  • Daftar produk (nama + harga) dengan tombol "Tambah ke Keranjang"
  • Keranjang di samping/bawah yang menampilkan item yang ditambahkan
  • Total harga di keranjang

Pikirkan: State apa yang dibutuhkan? Di mana state-nya tinggal?

💡 Hint
  • State: keranjang (array item yang ditambahkan)
  • State tinggal di App (parent dari DaftarProduk dan Keranjang)
  • DaftarProduk panggil onTambah(produk) → App update state keranjang
✅ Solusi
jsx
import { useState } from 'react';

const PRODUK = [
  { id: 1, nama: "Kaos Polos", harga: 75000 },
  { id: 2, nama: "Celana Jeans", harga: 250000 },
  { id: 3, nama: "Topi Baseball", harga: 50000 },
  { id: 4, nama: "Sepatu Sneakers", harga: 450000 },
];

function DaftarProduk({ onTambah }) {
  return (
    <div>
      <h3>Produk</h3>
      {PRODUK.map(p => (
        <div key={p.id} style={{ display: "flex", justifyContent: "space-between", padding: "8px 0" }}>
          <span>{p.nama} — Rp {p.harga.toLocaleString()}</span>
          <button onClick={() => onTambah(p)}>+ Keranjang</button>
        </div>
      ))}
    </div>
  );
}

function Keranjang({ items }) {
  const total = items.reduce((sum, item) => sum + item.harga, 0);

  return (
    <div style={{ borderTop: "2px solid #333", marginTop: "20px", paddingTop: "12px" }}>
      <h3>Keranjang ({items.length} item)</h3>
      {items.length === 0 ? (
        <p>Keranjang kosong</p>
      ) : (
        <>
          <ul>
            {items.map((item, i) => (
              <li key={i}>{item.nama} — Rp {item.harga.toLocaleString()}</li>
            ))}
          </ul>
          <p><strong>Total: Rp {total.toLocaleString()}</strong></p>
        </>
      )}
    </div>
  );
}

export default function App() {
  const [keranjang, setKeranjang] = useState([]);

  function handleTambah(produk) {
    setKeranjang([...keranjang, produk]);
  }

  return (
    <div style={{ maxWidth: "500px", margin: "20px auto" }}>
      <h2>Toko Online</h2>
      <DaftarProduk onTambah={handleTambah} />
      <Keranjang items={keranjang} />
    </div>
  );
}

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.