OPTIMISTIC-UI USER-EXPERIENCE FRONTEND ERROR-HANDLING CONFLICT-RESOLUTION DATA-FETCHING REACT-QUERY SWR WEB-DEVELOPMENT BEST-PRACTICES UI-UX RESPONSIVENESS STATE-MANAGEMENT

Optimistic UI Tingkat Lanjut: Strategi Penanganan Konflik dan Error untuk Pengalaman Pengguna yang Mulus

⏱️ 9 menit baca
👨‍💻

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):

  1. Frontend segera memperbarui UI seolah-olah aksi tersebut sudah berhasil. Misalnya, komentar baru langsung muncul di daftar.
  2. Backend menerima permintaan secara asynchronous.
  3. Jika backend berhasil, UI tetap pada keadaan yang diperbarui.
  4. Jika backend gagal, UI dikembalikan ke keadaan sebelumnya atau menampilkan pesan error.

Keuntungan:

Tantangan:

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:

❌ 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.

b. Conditional Updates (ETag, Versioning, Timestamp)

Ini adalah pendekatan yang lebih robust, di mana server memeriksa kondisi tertentu sebelum melakukan pembaruan.

📌 Cara kerja di frontend (dengan versioning):

  1. Frontend menampilkan data item A (versi 1).
  2. Pengguna mengubah item A. Frontend secara optimis menampilkan item A (versi 2).
  3. Permintaan update dikirim ke server, menyertakan version: 1.
  4. Skenario 1 (Berhasil): Server menerima version: 1, cocok dengan database. Server mengupdate item A menjadi versi 2 dan mengembalikan sukses. UI tetap versi 2.
  5. Skenario 2 (Konflik): Sementara itu, pengguna lain mengupdate item A menjadi versi 3. Server menerima version: 1 dari Anda, tetapi di database sudah version: 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:

  1. Mengembalikan UI ke state sebelum perubahan optimis Anda.
  2. Menampilkan pesan informatif kepada pengguna bahwa perubahan mereka tidak dapat disimpan karena konflik.
  3. 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):

  1. Kembalikan UI ke state sebelum update optimis.
  2. 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:

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.

📌 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