Bab 3: Berbagi State Antar Komponen
⏱ 5 menit bacaPendahuluan: Ketika Komponen Perlu "Ngobrol"
Bayangin kamu di kantor. Ada dua divisi: Divisi Marketing dan Divisi Produksi. Marketing dapet info dari klien bahwa pesanan berubah. Produksi perlu tau info ini supaya bisa adjust. Tapi mereka gak bisa langsung ngobrol... harus lewat manajer yang membawahi keduanya.
Di React, situasi yang sama terjadi. Kadang dua komponen yang sejajar (sibling) perlu berbagi informasi. Mereka gak bisa langsung "ngobrol" satu sama lain. Solusinya? Angkat state ke parent yang membawahi keduanya. Ini namanya "lifting state up".
Masalah: Dua Komponen Butuh Data yang Sama
Skenario: Panel Accordion
Bayangin kamu bikin FAQ page. Ada beberapa panel yang bisa dibuka/tutup. Tapi aturannya: cuma satu panel yang boleh terbuka pada satu waktu. Kalau panel A dibuka, panel B harus otomatis tertutup.
Kalau setiap panel punya state sendiri-sendiri:
// ❌ Setiap panel punya state sendiri - gak bisa koordinasi!
function Panel({ judul, isi }) {
const [terbuka, setTerbuka] = useState(false);
return (
<div>
<button onClick={() => setTerbuka(!terbuka)}>
{judul}
</button>
{terbuka && <p>{isi}</p>}
</div>
);
}
function HalamanFAQ() {
return (
<div>
{/* Kedua panel bisa terbuka bersamaan! Gak sesuai requirement */}
<Panel judul="Apa itu React?" isi="React adalah library JavaScript..." />
<Panel judul="Apa itu JSX?" isi="JSX adalah syntax extension..." />
</div>
);
}Masalahnya: Panel A gak tau kalau Panel B lagi terbuka. Mereka hidup di dunia masing-masing. Gak ada "manajer" yang koordinasi.
Solusi: Lifting State Up (Angkat State ke Atas)
Di rumah, ada satu remote TV. Siapa yang pegang remote, dia yang kontrol channel. Remote-nya disimpan di meja ruang keluarga (parent), bukan di kamar masing-masing anak (child).
Kalau Andi mau ganti channel, dia ambil remote dari meja. Kalau Budi mau ganti, dia juga ambil dari meja yang sama. Satu sumber kontrol, dipakai bersama.
Langkah-Langkah Lifting State Up
Langkah 1: Hapus state dari komponen child
// SEBELUM: Panel punya state sendiri
function Panel({ judul, isi }) {
const [terbuka, setTerbuka] = useState(false); // ← Hapus ini
// ...
}Langkah 2: Kirim data dari parent lewat props
// SESUDAH: Panel terima state dari parent
function Panel({ judul, isi, terbuka, onToggle }) {
// Gak punya state sendiri, terima dari props
return (
<div>
<button onClick={onToggle}>
{judul} {terbuka ? '▼' : '▶'}
</button>
{terbuka && <p>{isi}</p>}
</div>
);
}Langkah 3: Simpan state di parent (common ancestor)
function HalamanFAQ() {
// State disimpan di PARENT
const [indexAktif, setIndexAktif] = useState(null);
return (
<div>
<h2>❓ FAQ</h2>
<Panel
judul="Apa itu React?"
isi="React adalah library JavaScript untuk membangun UI."
terbuka={indexAktif === 0}
onToggle={() => setIndexAktif(indexAktif === 0 ? null : 0)}
/>
<Panel
judul="Apa itu JSX?"
isi="JSX adalah syntax extension yang memungkinkan kamu menulis HTML di JavaScript."
terbuka={indexAktif === 1}
onToggle={() => setIndexAktif(indexAktif === 1 ? null : 1)}
/>
<Panel
judul="Apa itu State?"
isi="State adalah data yang bisa berubah dan mempengaruhi tampilan komponen."
terbuka={indexAktif === 2}
onToggle={() => setIndexAktif(indexAktif === 2 ? null : 2)}
/>
</div>
);
}Coba sendiri: Edit kode di bawah dan lihat hasilnya langsung!
Sekarang, parent yang kontrol panel mana yang terbuka. Kalau panel 0 dibuka, otomatis panel lain tertutup karena indexAktif cuma bisa punya SATU nilai.
Controlled vs Uncontrolled Components
Uncontrolled (Mandiri): Karyawan yang kerja sendiri tanpa arahan detail dari bos. Dia punya "state internal" sendiri. Bos cuma tau hasilnya.
Controlled (Diawasi): Karyawan yang setiap langkahnya diarahkan bos. Dia gak punya keputusan sendiri, semua instruksi dari atas.
Dalam Kode
Uncontrolled Component:
// Komponen ini punya state SENDIRI
// Parent gak bisa kontrol apakah panel terbuka atau tidak
function PanelMandiri({ judul, isi }) {
const [terbuka, setTerbuka] = useState(false); // State internal
return (
<div>
<button onClick={() => setTerbuka(!terbuka)}>
{judul}
</button>
{terbuka && <p>{isi}</p>}
</div>
);
}
// Parent cuma "pasang" aja, gak bisa kontrol
function App() {
return <PanelMandiri judul="FAQ" isi="..." />;
// Gak bisa bilang: "Panel, buka dong!" dari sini
}Controlled Component:
// Komponen ini DIKONTROL oleh parent lewat props
// Gak punya state sendiri untuk buka/tutup
function PanelTerkontrol({ judul, isi, terbuka, onToggle }) {
return (
<div>
<button onClick={onToggle}>
{judul}
</button>
{terbuka && <p>{isi}</p>}
</div>
);
}
// Parent punya KONTROL PENUH
function App() {
const [buka, setBuka] = useState(false);
return (
<PanelTerkontrol
judul="FAQ"
isi="..."
terbuka={buka}
onToggle={() => setBuka(!buka)}
/>
);
// Parent bisa kapan aja bilang: setBuka(true) → panel terbuka!
}Kapan Pakai Mana?
| Situasi | Pilihan | Alasan |
|---|---|---|
| Komponen berdiri sendiri, gak perlu koordinasi | Uncontrolled | Lebih simpel, self-contained |
| Perlu koordinasi dengan komponen lain | Controlled | Parent bisa sinkronisasi |
| Komponen reusable (library) | Kasih opsi keduanya | Fleksibel untuk berbagai kebutuhan |
Single Source of Truth (Satu Sumber Kebenaran)
Di kantor yang baik, info penting ditempel di SATU papan pengumuman. Bukan di meja masing-masing karyawan. Kenapa? Karena kalau info berubah, cukup update di satu tempat. Semua orang lihat info yang sama.
Kalau setiap karyawan punya copy sendiri, bisa jadi: Andi punya info lama, Budi punya info baru. Kacau.
Dalam React
Untuk setiap "data" yang perlu di-share, tentukan SATU komponen yang jadi "pemilik" state tersebut. Komponen lain yang butuh data itu, terima lewat props.
// ✅ SATU sumber kebenaran: App punya data suhu
function App() {
const [suhuCelsius, setSuhuCelsius] = useState(25);
return (
<div>
<h1>🌡️ Konverter Suhu</h1>
<InputCelsius
suhu={suhuCelsius}
onChange={setSuhuCelsius}
/>
<InputFahrenheit
suhu={suhuCelsius}
onChange={setSuhuCelsius}
/>
<TampilanSuhu suhu={suhuCelsius} />
</div>
);
}
function InputCelsius({ suhu, onChange }) {
return (
<label>
Celsius:
<input
type="number"
value={suhu}
onChange={(e) => onChange(Number(e.target.value))}
/>
</label>
);
}
function InputFahrenheit({ suhu, onChange }) {
// Konversi untuk tampilan
const fahrenheit = (suhu * 9/5) + 32;
function handleChange(e) {
// Konversi balik ke Celsius sebelum kirim ke parent
const nilaiF = Number(e.target.value);
const nilaiC = (nilaiF - 32) * 5/9;
onChange(nilaiC);
}
return (
<label>
Fahrenheit:
<input
type="number"
value={Math.round(fahrenheit * 100) / 100}
onChange={handleChange}
/>
</label>
);
}
function TampilanSuhu({ suhu }) {
let deskripsi;
if (suhu < 0) deskripsi = '🥶 Beku!';
else if (suhu < 20) deskripsi = '🧥 Dingin';
else if (suhu < 30) deskripsi = '😊 Nyaman';
else deskripsi = '🥵 Panas!';
return <p>{deskripsi} ({suhu}°C)</p>;
}Perhatiin: suhuCelsius cuma ada di SATU tempat (App). InputCelsius dan InputFahrenheit keduanya "terhubung" ke sumber yang sama. Kalau satu berubah, yang lain ikut update.
Kapan Harus Lifting State Up?
Tanda-Tanda Kamu Perlu Lift State
-
Dua komponen sibling perlu data yang sama
- Contoh: Filter dan daftar produk perlu tau keyword pencarian
-
Aksi di satu komponen harus mempengaruhi komponen lain
- Contoh: Klik "Tambah ke Keranjang" di ProductCard harus update angka di CartIcon
-
Parent perlu tau state child
- Contoh: Form wizard dimana parent perlu tau apakah semua step sudah valid
Tanda-Tanda Kamu TIDAK Perlu Lift State
-
State cuma dipakai oleh satu komponen
- Contoh: Apakah dropdown terbuka atau tidak (cuma dropdown itu yang peduli)
-
State bersifat "UI-only" dan lokal
- Contoh: Posisi scroll, hover state, animasi internal
Contoh Lengkap: Aplikasi Pencarian Produk
import { useState } from 'react';
const PRODUK = [
{ id: 1, nama: 'Laptop ASUS', kategori: 'Elektronik', harga: 8500000, stok: true },
{ id: 2, nama: 'Mouse Logitech', kategori: 'Elektronik', harga: 350000, stok: true },
{ id: 3, nama: 'Meja Kerja', kategori: 'Furniture', harga: 1200000, stok: false },
{ id: 4, nama: 'Kursi Gaming', kategori: 'Furniture', harga: 2500000, stok: true },
{ id: 5, nama: 'Headset Sony', kategori: 'Elektronik', harga: 750000, stok: true },
{ id: 6, nama: 'Rak Buku', kategori: 'Furniture', harga: 450000, stok: false },
];
// PARENT: Pemilik state yang di-share
function AplikasiToko() {
// State yang di-share antara SearchBar dan ProductList
const [pencarian, setPencarian] = useState('');
const [cariStokAja, setCariStokAja] = useState(false);
const [kategoriAktif, setKategoriAktif] = useState('Semua');
// Derived: produk yang sudah difilter
const produkTerfilter = PRODUK.filter(p => {
const cocokNama = p.nama.toLowerCase().includes(pencarian.toLowerCase());
const cocokStok = cariStokAja ? p.stok : true;
const cocokKategori = kategoriAktif === 'Semua' || p.kategori === kategoriAktif;
return cocokNama && cocokStok && cocokKategori;
});
return (
<div>
<h1>🏪 Toko Online</h1>
{/* Child 1: Kontrol pencarian */}
<BarPencarian
pencarian={pencarian}
onPencarianBerubah={setPencarian}
cariStokAja={cariStokAja}
onStokBerubah={setCariStokAja}
kategoriAktif={kategoriAktif}
onKategoriBerubah={setKategoriAktif}
/>
{/* Child 2: Tampilkan hasil */}
<DaftarProduk produk={produkTerfilter} />
{/* Child 3: Info jumlah */}
<InfoHasil total={PRODUK.length} terfilter={produkTerfilter.length} />
</div>
);
}
// CHILD 1: Bar pencarian (controlled component)
function BarPencarian({
pencarian,
onPencarianBerubah,
cariStokAja,
onStokBerubah,
kategoriAktif,
onKategoriBerubah,
}) {
const kategoriList = ['Semua', 'Elektronik', 'Furniture'];
return (
<div style={{ marginBottom: '20px', padding: '15px', background: '#f5f5f5' }}>
<input
type="text"
placeholder="🔍 Cari produk..."
value={pencarian}
onChange={(e) => onPencarianBerubah(e.target.value)}
style={{ width: '100%', padding: '8px', marginBottom: '10px' }}
/>
<div>
<label>
<input
type="checkbox"
checked={cariStokAja}
onChange={(e) => onStokBerubah(e.target.checked)}
/>
Hanya yang tersedia
</label>
</div>
<div style={{ marginTop: '10px' }}>
{kategoriList.map(kat => (
<button
key={kat}
onClick={() => onKategoriBerubah(kat)}
style={{
marginRight: '5px',
padding: '5px 10px',
background: kategoriAktif === kat ? '#007bff' : '#ddd',
color: kategoriAktif === kat ? 'white' : 'black',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
{kat}
</button>
))}
</div>
</div>
);
}
// CHILD 2: Daftar produk
function DaftarProduk({ produk }) {
if (produk.length === 0) {
return <p>😕 Gak ada produk yang cocok dengan pencarian kamu.</p>;
}
return (
<div>
{produk.map(p => (
<div
key={p.id}
style={{
padding: '10px',
marginBottom: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
opacity: p.stok ? 1 : 0.5,
}}
>
<strong>{p.nama}</strong>
<span style={{ marginLeft: '10px', color: '#666' }}>{p.kategori}</span>
<span style={{ float: 'right' }}>
Rp {p.harga.toLocaleString()}
{!p.stok && <span style={{ color: 'red' }}> (Habis)</span>}
</span>
</div>
))}
</div>
);
}
// CHILD 3: Info hasil
function InfoHasil({ total, terfilter }) {
return (
<p style={{ color: '#666', marginTop: '10px' }}>
Menampilkan {terfilter} dari {total} produk
</p>
);
}Kenapa Struktur Ini Bekerja?
AplikasiToko (PEMILIK STATE)
├── pencarian, cariStokAja, kategoriAktif
├── produkTerfilter (derived)
│
├── BarPencarian (CONTROLLED - terima state + handler via props)
├── DaftarProduk (DISPLAY - terima data via props)
└── InfoHasil (DISPLAY - terima data via props)
BarPencarianmengubah state di parent lewat callback (onPencarianBerubah, dll)DaftarProdukdanInfoHasilcuma terima data yang sudah difilter- Semua komponen "sinkron" karena sumber datanya SATU
Pattern: Passing State + Handler sebagai Props
Ini pattern yang paling sering kamu pakai saat lifting state up:
// Parent: punya state + handler
function Parent() {
const [nilai, setNilai] = useState('');
function handleBerubah(nilaiBaru) {
// Bisa tambah logika di sini (validasi, transform, dll)
setNilai(nilaiBaru);
}
return (
<Child
nilai={nilai} // Kirim STATE ke bawah
onBerubah={handleBerubah} // Kirim HANDLER ke bawah
/>
);
}
// Child: terima state + handler dari parent
function Child({ nilai, onBerubah }) {
return (
<input
value={nilai}
onChange={(e) => onBerubah(e.target.value)}
/>
);
}Pattern ini kayak "remote control":
- State = channel TV saat ini (data yang ditampilkan)
- Handler = tombol remote (cara mengubah data)
Parent kasih keduanya ke child. Child bisa "lihat" data (lewat state) dan "ubah" data (lewat handler).
Contoh Lanjutan: Accordion yang Lebih Kompleks
import { useState } from 'react';
function Accordion() {
const [indexAktif, setIndexAktif] = useState(null);
const daftarFAQ = [
{
pertanyaan: 'Bagaimana cara mendaftar?',
jawaban: 'Klik tombol "Daftar" di pojok kanan atas, isi form dengan data yang valid, lalu klik "Submit". Kamu akan menerima email konfirmasi dalam 5 menit.',
},
{
pertanyaan: 'Berapa biaya berlangganan?',
jawaban: 'Kami punya 3 paket: Basic (gratis), Pro (Rp 99.000/bulan), dan Enterprise (hubungi sales). Semua paket bisa dicoba gratis 14 hari.',
},
{
pertanyaan: 'Bagaimana cara membatalkan langganan?',
jawaban: 'Masuk ke Settings → Billing → Cancel Subscription. Langganan kamu akan tetap aktif sampai akhir periode billing saat ini.',
},
{
pertanyaan: 'Apakah data saya aman?',
jawaban: 'Ya! Kami menggunakan enkripsi end-to-end dan server kami tersertifikasi ISO 27001. Data kamu gak akan dijual ke pihak ketiga.',
},
];
function handleToggle(index) {
// Kalau yang diklik sudah aktif, tutup (set null)
// Kalau beda, buka yang baru (otomatis tutup yang lama)
setIndexAktif(indexAktif === index ? null : index);
}
return (
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<h2>❓ Frequently Asked Questions</h2>
{daftarFAQ.map((faq, index) => (
<PanelAccordion
key={index}
pertanyaan={faq.pertanyaan}
jawaban={faq.jawaban}
terbuka={indexAktif === index}
onToggle={() => handleToggle(index)}
/>
))}
</div>
);
}
function PanelAccordion({ pertanyaan, jawaban, terbuka, onToggle }) {
return (
<div style={{
border: '1px solid #ddd',
borderRadius: '8px',
marginBottom: '8px',
overflow: 'hidden',
}}>
<button
onClick={onToggle}
style={{
width: '100%',
padding: '15px',
textAlign: 'left',
background: terbuka ? '#e3f2fd' : 'white',
border: 'none',
cursor: 'pointer',
fontSize: '16px',
fontWeight: 'bold',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{pertanyaan}
<span style={{
transform: terbuka ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s',
}}>
▼
</span>
</button>
{terbuka && (
<div style={{
padding: '15px',
background: '#fafafa',
borderTop: '1px solid #eee',
}}>
{jawaban}
</div>
)}
</div>
);
}Contoh: Tab Navigation
import { useState } from 'react';
function TabNavigation() {
const [tabAktif, setTabAktif] = useState('profil');
return (
<div>
<h2>👤 Akun Saya</h2>
{/* Tab buttons */}
<TabBar tabAktif={tabAktif} onTabBerubah={setTabAktif} />
{/* Tab content */}
<TabContent tabAktif={tabAktif} />
</div>
);
}
function TabBar({ tabAktif, onTabBerubah }) {
const tabs = [
{ id: 'profil', label: '👤 Profil' },
{ id: 'pesanan', label: '📦 Pesanan' },
{ id: 'pengaturan', label: '⚙️ Pengaturan' },
];
return (
<div style={{ display: 'flex', borderBottom: '2px solid #ddd' }}>
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => onTabBerubah(tab.id)}
style={{
padding: '10px 20px',
border: 'none',
background: 'none',
cursor: 'pointer',
borderBottom: tabAktif === tab.id ? '3px solid #007bff' : 'none',
fontWeight: tabAktif === tab.id ? 'bold' : 'normal',
color: tabAktif === tab.id ? '#007bff' : '#666',
}}
>
{tab.label}
</button>
))}
</div>
);
}
function TabContent({ tabAktif }) {
switch (tabAktif) {
case 'profil':
return (
<div style={{ padding: '20px' }}>
<h3>Profil Saya</h3>
<p>Nama: Budi Santoso</p>
<p>Email: budi@email.com</p>
<p>Bergabung: Januari 2024</p>
</div>
);
case 'pesanan':
return (
<div style={{ padding: '20px' }}>
<h3>Riwayat Pesanan</h3>
<p>📦 Pesanan #001 - Laptop ASUS - Dikirim</p>
<p>📦 Pesanan #002 - Mouse Logitech - Selesai</p>
</div>
);
case 'pengaturan':
return (
<div style={{ padding: '20px' }}>
<h3>Pengaturan</h3>
<p>🔔 Notifikasi: Aktif</p>
<p>🌙 Dark Mode: Nonaktif</p>
</div>
);
default:
return null;
}
}⚠️ Jebakan
Jebakan 1: Lift State Terlalu Tinggi
// ❌ State warna tema di-lift sampai ke App padahal cuma dipakai di Settings
function App() {
const [tema, setTema] = useState('light'); // Ini cuma dipakai di halaman Settings!
const [user, setUser] = useState(null);
const [notif, setNotif] = useState([]);
// ... 20 state lainnya
return (
<Router>
<Header />
<Settings tema={tema} onTemaBerubah={setTema} /> {/* Cuma di sini */}
<Footer />
</Router>
);
}
// ✅ Simpan state sedekat mungkin dengan yang membutuhkan
function Settings() {
const [tema, setTema] = useState('light'); // Cukup di sini!
return <TemaSelector tema={tema} onChange={setTema} />;
}Aturan: Lift state ke parent HANYA kalau memang ada >1 child yang butuh data itu.
Jebakan 2: Prop Drilling yang Berlebihan
// ❌ Props diteruskan melewati banyak level tanpa dipakai
function App() {
const [user, setUser] = useState({ nama: 'Budi' });
return <Layout user={user} />;
}
function Layout({ user }) {
// Layout gak pakai user, cuma nerusin
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
// Sidebar juga gak pakai, cuma nerusin
return <UserInfo user={user} />;
}
function UserInfo({ user }) {
// Baru di sini dipake!
return <p>Halo, {user.nama}!</p>;
}Kalau props harus melewati 3+ level tanpa dipakai di tengah, pertimbangkan Context (akan dibahas di Bab 6).
Jebakan 3: Lupa Kirim Handler ke Child
// ❌ Child terima state tapi gak bisa mengubahnya
function Parent() {
const [nilai, setNilai] = useState('');
return <Child nilai={nilai} />; // Mana handler-nya?
}
function Child({ nilai }) {
// Gak bisa update! Gak ada cara kirim perubahan ke parent
return <input value={nilai} onChange={???} />;
}
// ✅ Selalu kirim handler bersama state
function Parent() {
const [nilai, setNilai] = useState('');
return <Child nilai={nilai} onBerubah={setNilai} />;
}
function Child({ nilai, onBerubah }) {
return <input value={nilai} onChange={(e) => onBerubah(e.target.value)} />;
}Jebakan 4: Mengubah Props Langsung
// ❌ JANGAN ubah props! Props itu read-only
function Child({ data }) {
data.nama = 'Baru'; // SALAH! Ini mutasi props
return <p>{data.nama}</p>;
}
// ✅ Kalau mau ubah, panggil handler dari parent
function Child({ data, onUpdate }) {
function handleGantiNama() {
onUpdate({ ...data, nama: 'Baru' }); // Kirim data baru ke parent
}
return <button onClick={handleGantiNama}>Ganti Nama</button>;
}🏋️ Challenge
Challenge 1: Sinkronisasi Dua Input
Bikin dua input text yang selalu sinkron (apa yang diketik di satu, muncul di yang lain). Tapi input kedua menampilkan teks dalam UPPERCASE.
Hint: Lift state ke parent. Input kedua tampilkan teks.toUpperCase() tapi saat user ngetik di input kedua, convert balik ke lowercase sebelum update state.
Lihat Solusi
import { useState } from 'react';
function SinkronInput() {
// State di parent (single source of truth)
const [teks, setTeks] = useState('');
return (
<div>
<h2>🔄 Input Sinkron</h2>
<InputNormal
label="Normal"
nilai={teks}
onBerubah={setTeks}
/>
<InputUppercase
label="UPPERCASE"
nilai={teks}
onBerubah={setTeks}
/>
<p>Nilai state: "{teks}"</p>
</div>
);
}
function InputNormal({ label, nilai, onBerubah }) {
return (
<div style={{ marginBottom: '10px' }}>
<label>{label}: </label>
<input
value={nilai}
onChange={(e) => onBerubah(e.target.value)}
/>
</div>
);
}
function InputUppercase({ label, nilai, onBerubah }) {
return (
<div style={{ marginBottom: '10px' }}>
<label>{label}: </label>
<input
value={nilai.toUpperCase()}
onChange={(e) => onBerubah(e.target.value.toLowerCase())}
/>
</div>
);
}Challenge 2: Daftar dengan Filter dan Counter
Bikin aplikasi daftar tugas dimana:
- Komponen
FilterBarpunya tombol filter (Semua/Aktif/Selesai) - Komponen
TodoListmenampilkan tugas yang sudah difilter - Komponen
StatusBarmenampilkan "X dari Y tugas selesai"
Ketiga komponen harus sinkron (filter berubah → list berubah → counter berubah).
Hint: Semua state (todos + filter) ada di parent. Ketiga child adalah controlled components.
Lihat Solusi
import { useState } from 'react';
function AplikasiTugas() {
// State di parent
const [tugas, setTugas] = useState([
{ id: 1, teks: 'Belajar React', selesai: true },
{ id: 2, teks: 'Bikin portfolio', selesai: false },
{ id: 3, teks: 'Lamar kerja', selesai: false },
{ id: 4, teks: 'Baca dokumentasi', selesai: true },
]);
const [filter, setFilter] = useState('semua');
// Derived values
const tugasTerfilter = tugas.filter(t => {
if (filter === 'aktif') return !t.selesai;
if (filter === 'selesai') return t.selesai;
return true;
});
const jumlahSelesai = tugas.filter(t => t.selesai).length;
function toggleTugas(id) {
setTugas(tugas.map(t =>
t.id === id ? { ...t, selesai: !t.selesai } : t
));
}
return (
<div>
<h2>📋 Daftar Tugas</h2>
<FilterBar filter={filter} onFilterBerubah={setFilter} />
<TodoList tugas={tugasTerfilter} onToggle={toggleTugas} />
<StatusBar selesai={jumlahSelesai} total={tugas.length} />
</div>
);
}
function FilterBar({ filter, onFilterBerubah }) {
const opsi = ['semua', 'aktif', 'selesai'];
return (
<div style={{ marginBottom: '15px' }}>
{opsi.map(o => (
<button
key={o}
onClick={() => onFilterBerubah(o)}
style={{
marginRight: '5px',
padding: '5px 15px',
background: filter === o ? '#4CAF50' : '#eee',
color: filter === o ? 'white' : 'black',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
{o.charAt(0).toUpperCase() + o.slice(1)}
</button>
))}
</div>
);
}
function TodoList({ tugas, onToggle }) {
if (tugas.length === 0) {
return <p style={{ color: '#999' }}>Tidak ada tugas.</p>;
}
return (
<ul style={{ listStyle: 'none', padding: 0 }}>
{tugas.map(t => (
<li
key={t.id}
style={{ padding: '8px 0', borderBottom: '1px solid #eee' }}
>
<label style={{ cursor: 'pointer' }}>
<input
type="checkbox"
checked={t.selesai}
onChange={() => onToggle(t.id)}
/>
<span style={{
marginLeft: '8px',
textDecoration: t.selesai ? 'line-through' : 'none',
color: t.selesai ? '#999' : 'black',
}}>
{t.teks}
</span>
</label>
</li>
))}
</ul>
);
}
function StatusBar({ selesai, total }) {
const persen = total > 0 ? Math.round((selesai / total) * 100) : 0;
return (
<div style={{ marginTop: '15px', padding: '10px', background: '#f5f5f5', borderRadius: '4px' }}>
<p>{selesai} dari {total} tugas selesai ({persen}%)</p>
<div style={{ background: '#ddd', borderRadius: '4px', overflow: 'hidden' }}>
<div style={{
width: `${persen}%`,
height: '8px',
background: '#4CAF50',
transition: 'width 0.3s',
}} />
</div>
</div>
);
}Challenge 3: Color Picker Sinkron
Bikin color picker dimana:
- Ada input type="color" (native color picker)
- Ada 3 input number untuk R, G, B (0-255)
- Ada preview warna
- Semua harus sinkron: ubah color picker → RGB update. Ubah RGB → color picker update.
Hint: Simpan warna sebagai {r, g, b} di parent. Konversi ke hex untuk color picker, konversi dari hex saat color picker berubah.
Lihat Solusi
import { useState } from 'react';
// Helper: RGB ke Hex
function rgbKeHex(r, g, b) {
return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('');
}
// Helper: Hex ke RGB
function hexKeRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
} : { r: 0, g: 0, b: 0 };
}
function ColorPicker() {
// Single source of truth: warna dalam format RGB
const [warna, setWarna] = useState({ r: 66, g: 133, b: 244 });
// Derived: hex string
const hex = rgbKeHex(warna.r, warna.g, warna.b);
function handleHexBerubah(hexBaru) {
setWarna(hexKeRgb(hexBaru));
}
function handleRgbBerubah(channel, nilai) {
// Clamp antara 0-255
const nilaiValid = Math.max(0, Math.min(255, Number(nilai)));
setWarna({ ...warna, [channel]: nilaiValid });
}
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<h2>🎨 Color Picker</h2>
{/* Preview */}
<div style={{
width: '100%',
height: '100px',
backgroundColor: hex,
borderRadius: '8px',
marginBottom: '20px',
border: '2px solid #ddd',
}} />
{/* Native color picker */}
<div style={{ marginBottom: '15px' }}>
<label>Pilih Warna: </label>
<input
type="color"
value={hex}
onChange={(e) => handleHexBerubah(e.target.value)}
/>
<span style={{ marginLeft: '10px' }}>{hex}</span>
</div>
{/* RGB inputs */}
<InputRGB label="R (Red)" nilai={warna.r} warna="red"
onBerubah={(v) => handleRgbBerubah('r', v)} />
<InputRGB label="G (Green)" nilai={warna.g} warna="green"
onBerubah={(v) => handleRgbBerubah('g', v)} />
<InputRGB label="B (Blue)" nilai={warna.b} warna="blue"
onBerubah={(v) => handleRgbBerubah('b', v)} />
</div>
);
}
function InputRGB({ label, nilai, warna, onBerubah }) {
return (
<div style={{ marginBottom: '10px' }}>
<label style={{ display: 'inline-block', width: '80px', color: warna }}>
{label}:
</label>
<input
type="range"
min="0"
max="255"
value={nilai}
onChange={(e) => onBerubah(e.target.value)}
style={{ width: '150px', marginRight: '10px' }}
/>
<input
type="number"
min="0"
max="255"
value={nilai}
onChange={(e) => onBerubah(e.target.value)}
style={{ width: '60px' }}
/>
</div>
);
}Kesimpulan
Berbagi state antar komponen itu kayak sistem komunikasi di organisasi. Yang penting:
- Identifikasi komponen mana yang butuh data yang sama
- Temukan parent terdekat yang membawahi semua komponen tersebut
- Angkat state ke parent itu
- Kirim state + handler ke child lewat props
- Child jadi controlled component yang dikontrol parent
Ingat: lift state HANYA sejauh yang diperlukan. Jangan angkat ke App kalau cukup di level yang lebih rendah. Dan kalau prop drilling jadi terlalu dalam (3+ level), ada solusi yang lebih elegan yang bakal kita bahas di Bab 6 (Context).
Sudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.