Optimistic UI Tingkat Lanjut: Strategi Penanganan Konflik dan Error untuk Pengalaman Pengguna yang Mulus
1. Pendahuluan
Pernahkah Anda menggunakan aplikasi yang terasa begitu cepat dan responsif, seolah setiap interaksi langsung terwujud tanpa menunggu loading? Kemungkinan besar, Anda sedang merasakan keajaiban Optimistic UI. Optimistic UI adalah teknik desain antarmuka pengguna di mana aplikasi menampilkan hasil dari suatu aksi (misalnya, menambahkan item ke daftar, memberikan “like”) secara instan, sebelum mendapatkan konfirmasi dari server. Tujuannya? Memberikan pengalaman pengguna yang mulus, responsif, dan terasa “instan”, bahkan di jaringan yang lambat.
Namun, di balik kecepatan dan kemulusan itu, tersembunyi tantangan yang tidak sepele: bagaimana jika aksi yang ditampilkan secara optimis di frontend ternyata gagal di backend? Bagaimana jika ada konflik data ketika server akhirnya merespons? Artikel ini akan membawa Anda lebih dalam ke dunia Optimistic UI, tidak hanya sekadar implementasi dasar, tetapi juga strategi tingkat lanjut untuk menangani konflik data dan error agar aplikasi Anda tetap tangguh dan andal.
Siap membangun aplikasi yang tidak hanya cepat, tetapi juga cerdas dalam menghadapi ketidakpastian jaringan dan server? Mari kita selami!
2. Mengingat Kembali Optimistic UI: Fondasi Kecepatan
Secara singkat, Optimistic UI bekerja dengan asumsi bahwa setiap aksi pengguna akan berhasil. Ketika pengguna melakukan sesuatu yang melibatkan interaksi dengan server (misalnya, mengirim komentar):
- Frontend segera memperbarui UI seolah-olah aksi tersebut sudah berhasil. Misalnya, komentar baru langsung muncul di daftar.
- Backend menerima permintaan secara asynchronous.
- Jika backend berhasil, UI tetap pada keadaan yang diperbarui.
- Jika backend gagal, UI dikembalikan ke keadaan sebelumnya atau menampilkan pesan error.
✅ Keuntungan:
- Persepsi kecepatan: Pengguna merasa aplikasi sangat responsif.
- Pengalaman mulus: Mengurangi loading spinner atau state “menunggu” yang mengganggu.
- Mengurangi frustrasi: Pengguna dapat terus berinteraksi tanpa jeda.
❌ Tantangan:
- Penanganan error: Apa yang terjadi jika update optimis gagal? Bagaimana mengkomunikasikan ini kepada pengguna?
- Konflik data: Bagaimana jika data di server berubah (oleh pengguna lain) sebelum update optimis Anda dikonfirmasi?
- Kompleksitas state: Mengelola state lokal yang bersifat optimis bisa jadi rumit.
Artikel sebelumnya, “Membangun User Experience yang Responsif: Mengimplementasikan Optimistic UI”, mungkin telah membahas dasar-dasar implementasinya. Sekarang, kita akan fokus pada dua tantangan terbesar: penanganan konflik dan error.
3. Tantangan Nyata Optimistic UI: Konflik dan Error
Membangun Optimistic UI yang “bekerja” itu mudah. Membangun Optimistic UI yang “tangguh” di dunia nyata, di mana jaringan bisa putus, server bisa error, dan banyak pengguna berinteraksi secara bersamaan, itulah tantangan sebenarnya.
⚠️ Skenario Konflik Data
Bayangkan Anda sedang mengedit sebuah item di daftar belanjaan. Secara optimis, Anda melihat item tersebut sudah terbarui. Namun, di saat yang sama, teman Anda juga mengedit item yang sama dan menyimpannya ke server sebelum perubahan Anda sampai. Ketika server memproses perubahan Anda, ia mendeteksi adanya versi yang lebih baru dari data tersebut. Inilah konflik data.
Jika kita tidak menangani ini dengan baik, bisa terjadi:
- Perubahan teman Anda tertimpa oleh perubahan Anda yang sudah kadaluarsa (data loss).
- Perubahan Anda tidak disimpan, tetapi UI tetap menampilkan perubahan optimis Anda, menciptakan inkonsistensi.
❌ Skenario Penanganan Error yang Buruk
Anda mengklik tombol “Kirim Pesan”. Pesan Anda langsung muncul di chat. Namun, karena suatu alasan (misalnya, server kelebihan beban, validasi gagal, atau token autentikasi kadaluarsa), permintaan ke server gagal.
Jika kita hanya mengabaikan error, pesan Anda akan tetap terlihat di chat Anda, tetapi tidak pernah terkirim ke orang lain. Ini adalah pengalaman pengguna yang sangat buruk dan membingungkan.
🎯 Tujuan kita adalah membangun sistem yang bisa mengidentifikasi skenario ini dan meresponsnya dengan cerdas, menjaga integritas data sekaligus memberikan feedback yang jelas kepada pengguna.
4. Strategi Penanganan Konflik Data
Penanganan konflik dalam Optimistic UI sangat bergantung pada kebutuhan aplikasi Anda dan toleransi terhadap inkonsistensi.
a. Last-Write-Wins (Sederhana, tapi Berisiko)
Ini adalah strategi paling sederhana: perubahan terakhir yang diterima server akan menang. Jika ada konflik, perubahan sebelumnya akan ditimpa.
- Implementasi: Server tidak melakukan pemeriksaan versi. Cukup simpan data yang masuk.
- Kapan digunakan: Untuk data yang tidak terlalu kritis atau di mana konflik jarang terjadi dan dampaknya kecil (misalnya, perubahan status “seen” pada pesan).
- Risiko: Potensi data loss jika dua pengguna mengedit data yang sama secara bersamaan.
b. Conditional Updates (ETag, Versioning, Timestamp)
Ini adalah pendekatan yang lebih robust, di mana server memeriksa kondisi tertentu sebelum melakukan pembaruan.
- ETag (HTTP
If-Matchheader):- Saat mengambil data, server mengirimkan
ETag(identifikasi versi resource) di header respons. - Saat mengirim update, frontend menyertakan
ETagini di headerIf-Match. - Jika
ETagdi server tidak cocok denganIf-Matchdari klien (berarti data sudah berubah), server akan merespons dengan status412 Precondition Failed.
- Saat mengambil data, server mengirimkan
- Versioning (optimistic locking):
- Setiap record data di database memiliki kolom
version(integer) atauupdated_at(timestamp). - Saat mengambil data, frontend mendapatkan
versionatauupdated_atterbaru. - Saat mengirim update, frontend menyertakan
versionatauupdated_atyang didapat. - Server hanya akan mengupdate jika
versionatauupdated_atyang dikirim klien cocok dengan yang ada di database. Jika tidak, server mengembalikan error konflik.
- Setiap record data di database memiliki kolom
📌 Cara kerja di frontend (dengan versioning):
- Frontend menampilkan data item A (versi 1).
- Pengguna mengubah item A. Frontend secara optimis menampilkan item A (versi 2).
- Permintaan update dikirim ke server, menyertakan
version: 1. - Skenario 1 (Berhasil): Server menerima
version: 1, cocok dengan database. Server mengupdate item A menjadi versi 2 dan mengembalikan sukses. UI tetap versi 2. - Skenario 2 (Konflik): Sementara itu, pengguna lain mengupdate item A menjadi versi 3. Server menerima
version: 1dari Anda, tetapi di database sudahversion: 3. Server menolak update, mengembalikan error (misalnya,409 Conflict).
c. User Notification & Manual Resolution
Ketika konflik terdeteksi (misalnya, server merespons 409 Conflict dengan ETag atau versioning), aplikasi harus:
- Mengembalikan UI ke state sebelum perubahan optimis Anda.
- Menampilkan pesan informatif kepada pengguna bahwa perubahan mereka tidak dapat disimpan karena konflik.
- Memberikan opsi:
- “Lihat Perubahan Lain”: Menampilkan versi terbaru dari data (dari pengguna lain) dan versi perubahan pengguna saat ini, lalu membiarkan pengguna memilih atau menggabungkan secara manual.
- “Coba Lagi”: Mencoba menyimpan perubahan pengguna lagi, mungkin setelah mengambil versi terbaru dari server terlebih dahulu.
💡 Best Practice: Untuk data yang sering diedit secara kolaboratif, pertimbangkan untuk mengimplementasikan solusi penggabungan (merge) di sisi klien atau server.
5. Penanganan Error yang Elegan
Ketika update optimis gagal di backend, kita harus mengembalikan UI ke keadaan yang konsisten dan memberikan feedback yang jelas kepada pengguna.
a. Rollback Otomatis
Ini adalah respons default terbaik untuk sebagian besar skenario. Jika permintaan backend gagal (misalnya, status 500 Internal Server Error, 400 Bad Request, 401 Unauthorized, atau 409 Conflict):
- Kembalikan UI ke state sebelum update optimis.
- Tampilkan pesan error yang relevan kepada pengguna.
Contoh: Anda menambahkan item ke keranjang belanja. Item muncul secara optimis. Server merespons 400 Bad Request karena stok tidak tersedia. Item tersebut dihapus kembali dari keranjang di UI, dan muncul notifikasi “Stok barang tidak tersedia.”
b. Pesan Error Informatif
Pesan error harus:
- Jelas dan ringkas: Hindari jargon teknis.
- Memberikan konteks: Jelaskan apa yang salah (misalnya, “Nama produk sudah digunakan”, “Koneksi terputus”).
- Memberikan solusi (jika ada): “Silakan coba lagi”, “Periksa koneksi internet Anda”, “Hubungi dukungan”.
c. Retry Mechanism
Untuk error sementara (misalnya, 503 Service Unavailable, 429 Too Many Requests, atau masalah jaringan sementara), Anda bisa mengimplementasikan mekanisme retry dengan exponential backoff.
- Secara otomatis: Aplikasi mencoba lagi permintaan setelah jeda waktu tertentu yang semakin lama. Tampilkan indikator “mencoba ulang…”
- Secara manual: Jika retry otomatis gagal beberapa kali, berikan tombol “Coba Lagi” kepada pengguna.
📌 Penting: Pastikan permintaan Anda bersifat idempotent jika Anda menerapkan retry otomatis. Artinya, menjalankan permintaan yang sama berkali-kali tidak akan menyebabkan efek samping yang tidak diinginkan (misalnya, menambahkan item yang sama dua kali). Artikel “Idempotency dalam Sistem Terdistribusi” akan sangat membantu Anda memahami konsep ini.
6. Integrasi dengan Library Data Fetching (React Query / SWR)
Library seperti React Query (sekarang TanStack Query) dan SWR sangat menyederhanakan implementasi Optimistic UI dan penanganan konflik/error yang kompleks. Mereka menyediakan hook dan utilitas untuk mengelola cache data, mutasi, invalidasi, dan penanganan error secara otomatis.
Mari kita lihat contoh sederhana menggunakan React Query untuk menambahkan item ke daftar:
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
interface Todo {
id: string;
text: string;
completed: boolean;
}
// Simulasi API
const addTodoAPI = async (newTodoText: string): Promise<Todo> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Simulasi kegagalan 30% atau konflik
if (Math.random() < 0.3) {
reject(new Error('Gagal menambahkan todo. Coba lagi.'));
} else if (newTodoText.includes('konflik')) {
// Simulasi konflik (misalnya, item sudah ada)
reject(new Error('Todo dengan teks ini sudah ada (konflik).'));
}
resolve({ id: `todo-${Date.now()}`, text: newTodoText, completed: false });
}, 1000);
});
};
function TodoList() {
const queryClient = useQueryClient();
const [newTodoText, setNewTodoText] = useState('');
const mutation = useMutation({
mutationFn: addTodoAPI,
// 📌 onMutate: Ini adalah tempat Anda melakukan update optimis!
onMutate: async (textBaru) => {
// Batalkan query yang sedang berjalan untuk menghindari race condition
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Simpan nilai lama sebelum update optimis (untuk rollback)
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
// Lakukan update optimis ke cache
queryClient.setQueryData<Todo[]>(['todos'], (old) => {
const optimisticTodo = { id: 'optimistic-id', text: textBaru, completed: false };
return old ? [...old, optimisticTodo] : [optimisticTodo];
});
// Kembalikan konteks dengan data lama
return { previousTodos };
},
// ✅ onError: Ini adalah tempat Anda menangani kegagalan mutasi
onError: (err, textBaru, context) => {
console.error('Optimistic update