Masalah
Hari ini saya menemukan bug menarik di fitur bulk edit table. Pengguna mengeluhkan bahwa tombol "Save" menampilkan "1 changed row" meskipun mereka hanya mengetik teks di sebuah input field lalu menghapus semuanya kembali ke kondisi kosong. Seharusnya tidak ada perubahan karena nilainya sama seperti nilai awal, tapi sistem tetap mendeteksi adanya perubahan.
Petunjuk visualnya cukup jelas: tidak ada "blue dot" di sudut field mana pun (yang menandakan field yang berubah), tapi counter perubahan tetap menunjukkan 1 perubahan.
Analisis Root Cause
Setelah diselidiki, ternyata masalahnya ada di logika state management dari custom hook useBulkTableEdit. Hook ini melacak perubahan dengan menyimpan semua field yang pernah diupdate ke dalam sebuah object bernama editState:
// Sebelum diperbaiki const updateField = (itemId, field, value) => { dispatch({ type: "UPDATE_FIELD", itemId, field, value }); };
Reducer-nya sangat sederhana: setiap kali ada UPDATE_FIELD, langsung dimasukkan ke editState tanpa mengecek apakah nilainya berbeda dari nilai asli atau tidak.
case 'UPDATE_FIELD': return { ...state, editState: { ...state.editState, [action.itemId]: { ...state.editState[action.itemId], [action.field]: action.value, }, }, }
Masalahnya: Hook ini melacak "field yang sudah disentuh", bukan "field yang sudah berubah".
Jadi kalau pengguna mengetik "test" lalu menghapus semuanya kembali ke kosong (kembali ke nilai asli), field tersebut tetap tersimpan di editState dengan nilai yang sama seperti aslinya. Counter changesCount dihitung dari Object.keys(editState).length, yang menghitung item APA PUN di editState tanpa peduli apakah nilainya benar-benar berbeda.
Solusinya
Solusinya ternyata sederhana tapi butuh penempatan yang tepat secara strategis. Karena hook tidak punya akses ke nilai asli, saya memindahkan logika perbandingan ke level component β di mana kita punya akses ke data item aslinya.
Pattern yang Saya Terapkan
// Di editable components onChange={(event) => { const newValue = event.target.value const originalValue = item[field] // Bandingkan dan putuskan if (newValue === originalValue) { bulkEdit.removeField(item._id, field) // Hapus dari editState } else { bulkEdit.updateField(item._id, field, newValue) // Simpan di editState } // Validasi tetap berjalan onValidateField(newValue) }}
File yang Diubah
Saya menerapkan pattern ini ke semua editable component:
-
Reusable Components (untuk destinations):
EditableTextCell.tsx- Text input fieldsEditableCustomerCell.tsx- Dropdown select
-
Inline Edit Components:
customers.tsx- Field nama & emailoperators.tsx- Field nama
Pelajaran yang Didapat
1. Prinsip State Management
Lacak perubahan yang sebenarnya, bukan interaksi. Ada perbedaan besar antara "pengguna menyentuh field ini" vs "pengguna mengubah nilai field ini".
2. Data Access Pattern
Kalau sebuah hook tidak punya cukup konteks (dalam hal ini, nilai asli), dorong logikanya ke layer yang memilikinya β dalam hal ini, component yang me-render datanya.
3. Defensive Comparison
Selalu bandingkan dengan nilai asli sebelum mengupdate state:
// Pattern yang baik if (newValue === originalValue) { removeChange(); } else { recordChange(); } // Anti-pattern recordChange(); // Selalu merekam tanpa pengecekan
4. DRY via Components
Dengan membuat EditableTextCell dan EditableCustomerCell yang reusable, saya hanya perlu memperbaiki sekali untuk halaman destinations. Untuk inline edit di customers & operators, saya harus memperbaikinya satu per satu karena mereka tidak menggunakan shared component.
Strategi Testing
Setelah diperbaiki, saya menguji dengan skenario-skenario berikut:
- β Ketik teks β hapus semua β Tidak ada perubahan terdeteksi
- β Ketik teks β edit β ubah lagi β Hanya menampilkan perubahan jika berbeda
- β Ubah dropdown β kembalikan β Tidak ada perubahan terdeteksi
- β Indikator blue dot hanya muncul untuk field yang berbeda dari aslinya
- β Counter perubahan akurat sesuai jumlah perubahan yang sebenarnya
Semua lolos! π
Kesimpulan
Bug ini mengingatkan saya bahwa interaksi pengguna β perubahan data. Dalam UI/UX yang baik, sistem harus cukup cerdas untuk membedakan antara "pengguna sedang menjelajah/mengetik" vs "pengguna benar-benar membuat perubahan yang perlu disimpan".
Takeaway: Saat membangun edit interface, selalu validasi perubahan yang sebenarnya terhadap nilai asli, bukan hanya melacak interaksi. Pengguna kamu akan berterima kasih karena tidak diganggu dengan peringatan "unsaved changes" yang palsu! π
