Bab 6: Update Object di State
⏱ 5 menit bacaKenapa Object di State Itu Spesial?
Di bab-bab sebelumnya, kita pakai state dengan tipe sederhana: number, string, boolean. Sekarang kita masuk ke territory yang lebih tricky: object.
Aturan emas React: State harus diperlakukan sebagai immutable (nggak boleh dimutasi langsung).
Untuk number dan string, ini otomatis terjadi karena mereka memang immutable di JavaScript:
let angka = 5;
angka = 10; // Ini bukan mutasi, ini replacement (ganti total)
let nama = 'Budi';
nama = 'Ani'; // Juga replacementTapi object? Object itu mutable di JavaScript. Kamu BISA mengubah isinya tanpa mengganti referensinya:
const user = { nama: 'Budi', umur: 25 };
user.nama = 'Ani'; // Mutasi! Object yang sama, isi berubah
// user masih menunjuk ke object yang sama di memoriDan di sinilah masalahnya dengan React.
Masalah: Mutasi Nggak Trigger Re-render
import { useState } from 'react';
function ProfilBuruk() {
const [user, setUser] = useState({ nama: 'Budi', umur: 25 });
function handleUbahNama() {
user.nama = 'Ani'; // ❌ MUTASI LANGSUNG!
console.log(user); // { nama: 'Ani', umur: 25 } — berubah di memori
// Tapi layar NGGAK UPDATE! React nggak tau ada perubahan!
}
return (
<div>
<p>Nama: {user.nama}</p>
<p>Umur: {user.umur}</p>
<button onClick={handleUbahNama}>Ubah Nama</button>
</div>
);
}Kenapa layar nggak update? Karena kamu nggak pernah panggil setUser! React cuma tau state berubah kalau kamu panggil setter function. Dan bahkan kalau kamu panggil setUser(user) setelah mutasi, React mungkin skip re-render karena dia lihat "oh, object-nya sama" (referensi yang sama).
Analoginya: bayangin kamu punya foto KTP. Kamu coret-coret fotonya (mutasi), tapi nggak lapor ke Dukcapil (setState). Dukcapil (React) nggak tau KTP kamu berubah. Kamu harus bikin KTP baru (object baru) dan lapor (setState) supaya data di sistem update.
Solusi: Buat Object Baru (Spread Operator)
Cara yang benar: buat object baru yang berisi perubahan, lalu passing ke setter.
import { useState } from 'react';
function ProfilBaik() {
const [user, setUser] = useState({ nama: 'Budi', umur: 25 });
function handleUbahNama() {
// ✅ Buat object BARU dengan spread operator
setUser({
...user, // Copy semua properti lama
nama: 'Ani' // Timpa properti yang mau diubah
});
}
function handleUbahUmur() {
setUser({
...user,
umur: user.umur + 1
});
}
return (
<div>
<p>Nama: {user.nama}</p>
<p>Umur: {user.umur}</p>
<button onClick={handleUbahNama}>Ubah Nama ke Ani</button>
<button onClick={handleUbahUmur}>Tambah Umur</button>
</div>
);
}Spread Operator (...) Explained
Spread operator ... itu kayak "copy-paste semua isi":
const original = { nama: 'Budi', umur: 25, kota: 'Jakarta' };
// Spread = copy semua, lalu timpa yang perlu
const updated = {
...original, // nama: 'Budi', umur: 25, kota: 'Jakarta'
nama: 'Ani' // Timpa nama jadi 'Ani'
};
// Hasilnya: { nama: 'Ani', umur: 25, kota: 'Jakarta' }
// original TIDAK berubah!Analoginya: kamu fotokopi dokumen (spread), lalu coret dan tulis ulang satu bagian di fotokopian (override). Dokumen asli tetap utuh.
Penting: Urutan matters! Properti yang ditulis SETELAH spread akan menimpa:
const obj = { a: 1, b: 2, c: 3 };
// ✅ 'b' ditimpa jadi 99
const updated = { ...obj, b: 99 };
// Hasil: { a: 1, b: 99, c: 3 }
// ❌ Kalau spread SETELAH override, override ditimpa balik!
const wrong = { b: 99, ...obj };
// Hasil: { b: 2, a: 1, c: 3 } — b kembali ke 2!Contoh Praktis: Form Handling
Ini pola yang SANGAT sering dipakai di React — handle form dengan satu state object:
import { useState } from 'react';
function FormPendaftaran() {
const [formData, setFormData] = useState({
nama: '',
email: '',
telepon: '',
alamat: '',
kota: 'Jakarta'
});
// Satu handler untuk semua input!
function handleChange(e) {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value // Computed property name — key dinamis!
});
}
function handleSubmit(e) {
e.preventDefault();
console.log('Data form:', formData);
alert(`Pendaftaran berhasil untuk ${formData.nama}!`);
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>Nama:</label>
<input
name="nama"
value={formData.nama}
onChange={handleChange}
/>
</div>
<div>
<label>Email:</label>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<div>
<label>Telepon:</label>
<input
name="telepon"
value={formData.telepon}
onChange={handleChange}
/>
</div>
<div>
<label>Alamat:</label>
<input
name="alamat"
value={formData.alamat}
onChange={handleChange}
/>
</div>
<div>
<label>Kota:</label>
<select name="kota" value={formData.kota} onChange={handleChange}>
<option value="Jakarta">Jakarta</option>
<option value="Bandung">Bandung</option>
<option value="Surabaya">Surabaya</option>
<option value="Yogyakarta">Yogyakarta</option>
</select>
</div>
<button type="submit">Daftar</button>
<pre>{JSON.stringify(formData, null, 2)}</pre>
</form>
);
}Computed Property Names: [name]: value
Ini fitur JavaScript (bukan React) yang super berguna:
const key = 'nama';
const value = 'Budi';
// Tanpa computed property:
const obj1 = {};
obj1[key] = value; // { nama: 'Budi' }
// Dengan computed property (lebih ringkas):
const obj2 = { [key]: value }; // { nama: 'Budi' }Di form handler, e.target.name berisi nama field (misalnya "email"), dan e.target.value berisi nilainya. Jadi [name]: value secara dinamis mengupdate field yang tepat.
Update Nested Object (Object di Dalam Object)
Ini yang mulai tricky. Kalau object kamu punya object di dalamnya:
const [user, setUser] = useState({
nama: 'Budi',
alamat: {
jalan: 'Jl. Merdeka No. 10',
kota: 'Jakarta',
provinsi: 'DKI Jakarta'
}
});Gimana cara update kota di dalam alamat?
// ❌ SALAH — mutasi nested object
function handleUbahKota() {
user.alamat.kota = 'Bandung'; // MUTASI!
setUser(user); // React mungkin skip karena referensi sama
}
// ❌ MASIH SALAH — spread level pertama aja nggak cukup
function handleUbahKota() {
setUser({
...user,
// alamat masih referensi ke object yang sama!
});
user.alamat.kota = 'Bandung'; // Masih mutasi!
}
// ✅ BENAR — spread di SETIAP level yang berubah
function handleUbahKota() {
setUser({
...user, // Copy level 1
alamat: {
...user.alamat, // Copy level 2
kota: 'Bandung' // Update field yang diinginkan
}
});
}Analoginya: bayangin kamu punya kotak (object luar) yang di dalamnya ada kotak kecil (nested object). Kalau mau ganti isi kotak kecil, kamu harus:
- Bikin kotak besar baru (spread level 1)
- Bikin kotak kecil baru di dalamnya (spread level 2)
- Isi kotak kecil baru dengan yang diinginkan
Kamu nggak bisa cuma bikin kotak besar baru tapi pakai kotak kecil yang sama — karena kotak kecil itu masih referensi ke yang lama.
Contoh Lengkap: Nested Object
import { useState } from 'react';
function ProfilLengkap() {
const [profil, setProfil] = useState({
nama: 'Budi Santoso',
pekerjaan: {
perusahaan: 'Warung Kopi Digital',
posisi: 'Barista Senior',
gaji: 5000000
},
alamat: {
jalan: 'Jl. Kopi Susu No. 42',
kota: 'Jakarta',
kodePos: '12345'
}
});
function handleUbahPosisi() {
setProfil({
...profil,
pekerjaan: {
...profil.pekerjaan,
posisi: 'Head Barista'
}
});
}
function handleNaikGaji() {
setProfil({
...profil,
pekerjaan: {
...profil.pekerjaan,
gaji: profil.pekerjaan.gaji + 500000
}
});
}
function handlePindahKota(kotaBaru) {
setProfil({
...profil,
alamat: {
...profil.alamat,
kota: kotaBaru
}
});
}
return (
<div>
<h2>{profil.nama}</h2>
<p>Posisi: {profil.pekerjaan.posisi} di {profil.pekerjaan.perusahaan}</p>
<p>Gaji: Rp {profil.pekerjaan.gaji.toLocaleString()}</p>
<p>Alamat: {profil.alamat.jalan}, {profil.alamat.kota}</p>
<button onClick={handleUbahPosisi}>Promosi!</button>
<button onClick={handleNaikGaji}>Naik Gaji +500rb</button>
<button onClick={() => handlePindahKota('Bandung')}>Pindah ke Bandung</button>
</div>
);
}Immer: Solusi untuk Nested Object yang Kompleks
Kalau object kamu deeply nested (banyak level), spread operator jadi ribet banget:
// Ini PAIN kalau nested-nya dalam:
setProfil({
...profil,
perusahaan: {
...profil.perusahaan,
cabang: {
...profil.perusahaan.cabang,
jakarta: {
...profil.perusahaan.cabang.jakarta,
alamat: 'Jl. Baru No. 1'
}
}
}
});Immer adalah library yang memungkinkan kamu "menulis kode mutasi" tapi di belakang layar dia bikin object baru. Magic!
Install Immer
npm install immer use-immerPakai useImmer (pengganti useState)
import { useImmer } from 'use-immer';
function ProfilDenganImmer() {
const [profil, updateProfil] = useImmer({
nama: 'Budi Santoso',
pekerjaan: {
perusahaan: 'Warung Kopi Digital',
posisi: 'Barista Senior',
gaji: 5000000
},
alamat: {
jalan: 'Jl. Kopi Susu No. 42',
kota: 'Jakarta',
kodePos: '12345'
}
});
function handleUbahPosisi() {
// Dengan Immer, kamu BISA "mutasi" — Immer yang handle sisanya
updateProfil(draft => {
draft.pekerjaan.posisi = 'Head Barista';
});
}
function handleNaikGaji() {
updateProfil(draft => {
draft.pekerjaan.gaji += 500000;
});
}
function handlePindahKota(kotaBaru) {
updateProfil(draft => {
draft.alamat.kota = kotaBaru;
});
}
return (
<div>
<h2>{profil.nama}</h2>
<p>Posisi: {profil.pekerjaan.posisi}</p>
<p>Gaji: Rp {profil.pekerjaan.gaji.toLocaleString()}</p>
<p>Kota: {profil.alamat.kota}</p>
<button onClick={handleUbahPosisi}>Promosi!</button>
<button onClick={handleNaikGaji}>Naik Gaji</button>
<button onClick={() => handlePindahKota('Bandung')}>Pindah</button>
</div>
);
}Gimana Immer Bekerja?
Immer bikin "draft" (salinan sementara) dari state kamu. Kamu boleh "mutasi" draft sesuka hati. Setelah fungsi selesai, Immer membandingkan draft dengan original dan bikin object baru yang berisi perubahan. Object asli TIDAK pernah dimutasi.
// Kamu tulis ini (terlihat kayak mutasi):
updateProfil(draft => {
draft.pekerjaan.gaji += 500000;
});
// Immer di belakang layar melakukan ini:
setProfil({
...profil,
pekerjaan: {
...profil.pekerjaan,
gaji: profil.pekerjaan.gaji + 500000
}
});Analoginya: Immer itu kayak asisten yang kamu kasih instruksi "coret ini, ganti itu" di fotokopi dokumen. Dia yang repot bikin dokumen baru yang rapi. Kamu cuma kasih instruksi.
Pakai Immer tanpa use-immer (dengan produce)
Kalau nggak mau install use-immer, bisa pakai produce dari immer langsung:
import { useState } from 'react';
import { produce } from 'immer';
function Contoh() {
const [state, setState] = useState({ nested: { value: 0 } });
function handleUpdate() {
setState(produce(draft => {
draft.nested.value += 1;
}));
}
}Pola-Pola Umum Update Object
1. Update Satu Field
const [user, setUser] = useState({ nama: 'Budi', umur: 25 });
// Update nama
setUser({ ...user, nama: 'Ani' });2. Update Multiple Fields Sekaligus
setUser({
...user,
nama: 'Ani',
umur: 30,
kota: 'Bandung'
});3. Update Field Berdasarkan Nilai Lama
// Increment umur
setUser({ ...user, umur: user.umur + 1 });
// Toggle boolean
setSettings({ ...settings, darkMode: !settings.darkMode });4. Hapus Field dari Object
const [data, setData] = useState({ a: 1, b: 2, c: 3 });
// Hapus field 'b'
function handleHapusB() {
const { b, ...sisanya } = data; // Destructure 'b' keluar
setData(sisanya); // Set state ke sisanya (tanpa 'b')
}
// Hasil: { a: 1, c: 3 }5. Update Dinamis (Computed Property)
function handleChange(field, value) {
setUser({
...user,
[field]: value // field bisa 'nama', 'umur', 'email', dll
});
}
// Pemakaian:
handleChange('nama', 'Budi');
handleChange('umur', 30);6. Merge Object
const [config, setConfig] = useState({ tema: 'light', bahasa: 'id' });
function handleUpdateConfig(updates) {
setConfig({
...config,
...updates // Merge semua perubahan sekaligus
});
}
// Pemakaian:
handleUpdateConfig({ tema: 'dark', fontSize: 16 });
// Hasil: { tema: 'dark', bahasa: 'id', fontSize: 16 }Contoh Lengkap: Aplikasi Profil dengan Edit Mode
import { useState } from 'react';
function ProfilApp() {
const [profil, setProfil] = useState({
namaDepan: 'Budi',
namaBelakang: 'Santoso',
email: 'budi@warungkopi.id',
bio: 'Barista yang suka ngoding',
avatar: '☕'
});
const [sedangEdit, setSedangEdit] = useState(false);
const [draft, setDraft] = useState(profil);
function handleMulaiEdit() {
setDraft(profil); // Copy profil ke draft
setSedangEdit(true);
}
function handleSimpan() {
setProfil(draft); // Apply draft ke profil
setSedangEdit(false);
}
function handleBatal() {
setDraft(profil); // Reset draft ke profil asli
setSedangEdit(false);
}
function handleDraftChange(e) {
const { name, value } = e.target;
setDraft({
...draft,
[name]: value
});
}
if (sedangEdit) {
return (
<div>
<h2>Edit Profil</h2>
<div>
<label>Nama Depan:</label>
<input name="namaDepan" value={draft.namaDepan} onChange={handleDraftChange} />
</div>
<div>
<label>Nama Belakang:</label>
<input name="namaBelakang" value={draft.namaBelakang} onChange={handleDraftChange} />
</div>
<div>
<label>Email:</label>
<input name="email" value={draft.email} onChange={handleDraftChange} />
</div>
<div>
<label>Bio:</label>
<textarea name="bio" value={draft.bio} onChange={handleDraftChange} />
</div>
<button onClick={handleSimpan}>Simpan</button>
<button onClick={handleBatal}>Batal</button>
</div>
);
}
return (
<div>
<h2>{profil.avatar} {profil.namaDepan} {profil.namaBelakang}</h2>
<p>Email: {profil.email}</p>
<p>Bio: {profil.bio}</p>
<button onClick={handleMulaiEdit}>Edit Profil</button>
</div>
);
}Kenapa Immutability Itu Penting?
Mungkin kamu mikir "Ribet amat, kenapa nggak mutasi aja?" Ada beberapa alasan kuat:
1. React Bisa Mendeteksi Perubahan
React membandingkan state lama vs baru dengan === (referensi). Kalau kamu mutasi object yang sama, referensinya nggak berubah, jadi React pikir "nggak ada yang berubah" dan skip re-render.
// ❌ Referensi sama — React skip re-render
const obj = { nama: 'Budi' };
obj.nama = 'Ani';
setState(obj); // obj === obj → true → React: "sama aja, skip"
// ✅ Referensi beda — React tau ada perubahan
const objBaru = { ...obj, nama: 'Ani' };
setState(objBaru); // objBaru !== obj → true → React: "beda! re-render!"2. Debugging Lebih Mudah
Dengan immutability, kamu bisa "time travel" — lihat state di setiap titik waktu. Kalau mutasi, state lama sudah hilang (ditimpa).
3. Fitur React yang Bergantung pada Immutability
React.memo— bandingkan props lama vs baruuseMemo/useCallback— dependency comparison- Concurrent features — React bisa "membatalkan" render yang belum selesai
4. Predictability
Kode immutable lebih mudah diprediksi. Kamu tau bahwa object yang sudah dibuat nggak akan berubah secara tiba-tiba dari tempat lain.
⚠️ Jebakan
Jebakan 1: Spread cuma shallow copy
const [data, setData] = useState({
info: { nama: 'Budi' },
settings: { tema: 'dark' }
});
// ❌ Spread cuma copy level pertama!
const copy = { ...data };
copy.info.nama = 'Ani'; // MUTASI! data.info.nama juga berubah!
// ✅ Harus spread di setiap level yang diubah
setData({
...data,
info: { ...data.info, nama: 'Ani' }
});Spread operator cuma melakukan shallow copy (copy level pertama). Nested object masih berbagi referensi!
Jebakan 2: Lupa spread sebelum override
const [user, setUser] = useState({ nama: 'Budi', umur: 25, kota: 'Jakarta' });
// ❌ Properti lain hilang!
setUser({ nama: 'Ani' });
// Hasil: { nama: 'Ani' } — umur dan kota HILANG!
// ✅ Spread dulu, baru override
setUser({ ...user, nama: 'Ani' });
// Hasil: { nama: 'Ani', umur: 25, kota: 'Jakarta' }Jebakan 3: Mutasi di luar setState "terasa" aman tapi berbahaya
// ❌ Ini terlihat oke tapi BERBAHAYA
function handleKlik() {
const userBaru = user; // Ini BUKAN copy! Ini referensi yang sama!
userBaru.nama = 'Ani'; // Mutasi object asli!
setUser(userBaru); // Referensi sama, React mungkin skip
}
// ✅ Buat object baru yang benar-benar baru
function handleKlik() {
const userBaru = { ...user, nama: 'Ani' }; // Object baru!
setUser(userBaru);
}Jebakan 4: Array di dalam object
const [data, setData] = useState({
nama: 'Budi',
hobi: ['coding', 'kopi', 'baca']
});
// ❌ Push mutasi array!
function handleTambahHobi() {
data.hobi.push('gaming'); // MUTASI!
setData({ ...data }); // Spread level 1 nggak cukup!
}
// ✅ Buat array baru
function handleTambahHobi() {
setData({
...data,
hobi: [...data.hobi, 'gaming'] // Array baru!
});
}Jebakan 5: Event handler yang "menumpuk" state lama
// ❌ Bug: handleChange pakai 'formData' dari closure
// Kalau user ketik cepat, bisa kehilangan input
function handleChange(e) {
const { name, value } = e.target;
setFormData({
...formData, // Ini snapshot! Bisa basi kalau ada update lain
[name]: value
});
}
// ✅ Pakai updater function untuk aman
function handleChange(e) {
const { name, value } = e.target;
setFormData(prev => ({
...prev, // Selalu pakai nilai terbaru
[name]: value
}));
}🏋️ Challenge
Challenge 1: Form Alamat Nested
Buat form yang mengedit data berikut. Semua field harus bisa diedit dan perubahan langsung terlihat di preview:
const dataAwal = {
nama: 'Budi',
kontak: {
email: 'budi@mail.com',
telepon: '08123456789'
},
alamat: {
jalan: 'Jl. Merdeka No. 10',
kota: 'Jakarta',
kodePos: '12345'
}
};💡 Hint
Untuk nested object, kamu perlu spread di setiap level. Buat handler terpisah untuk setiap "grup" (kontak, alamat), atau buat satu handler generik yang menerima path.
✅ Solusi
import { useState } from 'react';
function FormAlamat() {
const [data, setData] = useState({
nama: 'Budi',
kontak: {
email: 'budi@mail.com',
telepon: '08123456789'
},
alamat: {
jalan: 'Jl. Merdeka No. 10',
kota: 'Jakarta',
kodePos: '12345'
}
});
function handleNamaChange(e) {
setData({ ...data, nama: e.target.value });
}
function handleKontakChange(e) {
const { name, value } = e.target;
setData({
...data,
kontak: {
...data.kontak,
[name]: value
}
});
}
function handleAlamatChange(e) {
const { name, value } = e.target;
setData({
...data,
alamat: {
...data.alamat,
[name]: value
}
});
}
return (
<div>
<h2>Edit Data</h2>
<div>
<label>Nama:</label>
<input value={data.nama} onChange={handleNamaChange} />
</div>
<h3>Kontak</h3>
<div>
<label>Email:</label>
<input name="email" value={data.kontak.email} onChange={handleKontakChange} />
</div>
<div>
<label>Telepon:</label>
<input name="telepon" value={data.kontak.telepon} onChange={handleKontakChange} />
</div>
<h3>Alamat</h3>
<div>
<label>Jalan:</label>
<input name="jalan" value={data.alamat.jalan} onChange={handleAlamatChange} />
</div>
<div>
<label>Kota:</label>
<input name="kota" value={data.alamat.kota} onChange={handleAlamatChange} />
</div>
<div>
<label>Kode Pos:</label>
<input name="kodePos" value={data.alamat.kodePos} onChange={handleAlamatChange} />
</div>
<h3>Preview:</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}Challenge 2: Settings Panel dengan Toggle
Buat panel settings dengan beberapa toggle (boolean) yang disimpan dalam satu object state:
const settingsAwal = {
darkMode: false,
notifikasi: true,
suara: true,
autoSave: false,
bahasa: 'id'
};Setiap toggle harus bisa diklik untuk flip nilainya. Tampilkan status semua settings.
💡 Hint
Untuk toggle boolean, pakai !nilaiSekarang. Buat satu handler generik yang menerima nama field.
✅ Solusi
import { useState } from 'react';
function SettingsPanel() {
const [settings, setSettings] = useState({
darkMode: false,
notifikasi: true,
suara: true,
autoSave: false,
bahasa: 'id'
});
function handleToggle(field) {
setSettings(prev => ({
...prev,
[field]: !prev[field]
}));
}
function handleBahasaChange(e) {
setSettings(prev => ({
...prev,
bahasa: e.target.value
}));
}
const toggleFields = [
{ key: 'darkMode', label: 'Mode Gelap' },
{ key: 'notifikasi', label: 'Notifikasi' },
{ key: 'suara', label: 'Suara' },
{ key: 'autoSave', label: 'Auto Save' },
];
return (
<div style={{
backgroundColor: settings.darkMode ? '#1a1a2e' : '#fff',
color: settings.darkMode ? '#eee' : '#333',
padding: '20px',
borderRadius: '8px'
}}>
<h2>Settings</h2>
{toggleFields.map(({ key, label }) => (
<div key={key} style={{ marginBottom: '10px' }}>
<label>
<input
type="checkbox"
checked={settings[key]}
onChange={() => handleToggle(key)}
/>
{' '}{label}: {settings[key] ? '✅ ON' : '❌ OFF'}
</label>
</div>
))}
<div style={{ marginTop: '10px' }}>
<label>Bahasa: </label>
<select value={settings.bahasa} onChange={handleBahasaChange}>
<option value="id">Indonesia</option>
<option value="en">English</option>
<option value="jp">日本語</option>
</select>
</div>
<pre style={{ marginTop: '20px', fontSize: '12px' }}>
{JSON.stringify(settings, null, 2)}
</pre>
</div>
);
}Challenge 3: Kanban Card yang Bisa Diedit
Buat kartu Kanban yang menampilkan task. Kartu punya mode "view" dan mode "edit". Di mode edit, user bisa mengubah judul, deskripsi, dan prioritas. Ada tombol Save dan Cancel.
💡 Hint
Pakai pola "draft state" — saat mulai edit, copy state ke draft. Edit di draft. Save = apply draft ke state asli. Cancel = buang draft.
✅ Solusi
import { useState } from 'react';
function KanbanCard() {
const [task, setTask] = useState({
judul: 'Bikin fitur login',
deskripsi: 'Implementasi login dengan email dan password',
prioritas: 'tinggi',
status: 'in-progress'
});
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(task);
function handleEdit() {
setDraft(task); // Copy current state ke draft
setEditing(true);
}
function handleSave() {
setTask(draft); // Apply draft ke state asli
setEditing(false);
}
function handleCancel() {
setDraft(task); // Reset draft
setEditing(false);
}
function handleDraftChange(e) {
const { name, value } = e.target;
setDraft(prev => ({
...prev,
[name]: value
}));
}
const warnaPrioritas = {
rendah: '#4caf50',
sedang: '#ff9800',
tinggi: '#f44336'
};
if (editing) {
return (
<div style={{ border: '2px solid #2196f3', padding: '16px', borderRadius: '8px' }}>
<h3>Edit Task</h3>
<div>
<label>Judul:</label>
<input name="judul" value={draft.judul} onChange={handleDraftChange} style={{ width: '100%' }} />
</div>
<div>
<label>Deskripsi:</label>
<textarea name="deskripsi" value={draft.deskripsi} onChange={handleDraftChange} style={{ width: '100%' }} />
</div>
<div>
<label>Prioritas:</label>
<select name="prioritas" value={draft.prioritas} onChange={handleDraftChange}>
<option value="rendah">Rendah</option>
<option value="sedang">Sedang</option>
<option value="tinggi">Tinggi</option>
</select>
</div>
<div>
<label>Status:</label>
<select name="status" value={draft.status} onChange={handleDraftChange}>
<option value="todo">Todo</option>
<option value="in-progress">In Progress</option>
<option value="done">Done</option>
</select>
</div>
<button onClick={handleSave}>💾 Simpan</button>
<button onClick={handleCancel}>❌ Batal</button>
</div>
);
}
return (
<div style={{
border: '1px solid #ddd',
padding: '16px',
borderRadius: '8px',
borderLeft: `4px solid ${warnaPrioritas[task.prioritas]}`
}}>
<h3>{task.judul}</h3>
<p>{task.deskripsi}</p>
<span style={{
backgroundColor: warnaPrioritas[task.prioritas],
color: 'white',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '12px'
}}>
{task.prioritas}
</span>
{' '}
<span style={{ fontSize: '12px', color: '#666' }}>
[{task.status}]
</span>
<br /><br />
<button onClick={handleEdit}>✏️ Edit</button>
</div>
);
}Ringkasan
- Jangan mutasi object di state — selalu buat object baru
- Pakai spread operator (
...) untuk copy dan override properti - Untuk nested object, spread di SETIAP level yang berubah
- Computed property names (
[key]: value) berguna untuk handler dinamis - Immer menyederhanakan update nested object yang kompleks
- Pola draft state berguna untuk fitur edit/cancel
- Spread cuma shallow copy — nested object masih berbagi referensi
- Selalu pastikan
setStatemenerima object baru (referensi berbeda)
Di bab terakhir part ini, kita bakal bahas cara update array di state. Array juga reference type, jadi aturan immutability yang sama berlaku. Tapi array punya operasi-operasi khasnya sendiri (tambah, hapus, urutkan, dll) yang perlu pendekatan khusus.
Sudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.