Bab 2: Thinking in React — Cara Berpikir ala React
⏱ 5 menit bacaKenapa 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.
- 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:
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:
- Apakah bagian ini punya tanggung jawab sendiri? → Kalau ya, jadikan component.
- Apakah bagian ini bisa di-reuse? → Kalau ya, jadikan component.
- Apakah bagian ini terlalu kompleks kalau digabung? → Kalau ya, pecah.
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.
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:
- Daftar produk asli
- Teks yang diketik user di search
- Nilai checkbox (centang/tidak)
- 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
| Data | State? | Alasan |
|---|---|---|
| Daftar produk asli | ❌ | Dikirim lewat props |
| Teks search | ✅ | Berubah saat user ketik, nggak bisa dihitung |
| Nilai checkbox | ✅ | Berubah saat user centang, nggak bisa dihitung |
| Daftar produk terfilter | ❌ | Bisa dihitung dari produk + search + checkbox |
Kesimpulan: Cuma butuh 2 state — teksFilter dan hanyaStok.
- 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:
- Cari semua component yang butuh state itu
- Cari parent terdekat yang mencakup semuanya
- Taruh state di situ
Untuk kasus kita:
SearchBarbutuhteksFilterdanhanyaStok(untuk tampilkan di input)TabelProdukbutuhteksFilterdanhanyaStok(untuk filter data)- Parent terdekat keduanya =
FilterableProdukTable
Jadi state tinggal di FilterableProdukTable!
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>
);
}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.
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")
- 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
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
| Langkah | Aksi | Hasil |
|---|---|---|
| 1 | Pecah UI jadi component | Hierarki component |
| 2 | Bangun versi statis | UI tanpa interaksi |
| 3 | Tentukan state minimal | Daftar state yang dibutuhkan |
| 4 | Tentukan lokasi state | State di component yang tepat |
| 5 | Tambah inverse data flow | App interaktif penuh |
⚠️ Jebakan
1. Terlalu banyak state
// ❌ 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
Challenge 1: Daftar Kontak dengan Search
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:
- Pecah jadi component (SearchInput, KontakList, KontakItem)
- Bangun statis dulu
- State minimal: teks search
- State di parent
- 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
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
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.