Pembaruan UI optimistis di React bisa membuat aplikasi terasa instan. Pelajari pola aman untuk merekonsiliasi dengan kebenaran server, menangani kegagalan, dan mencegah penyimpangan data.

Optimistic UI di React berarti Anda memperbarui tampilan seolah-olah perubahan sudah berhasil, sebelum server mengonfirmasi. Seseorang mengklik Like, hitungan langsung melonjak, dan permintaan berjalan di latar.
Umpan balik instan itu membuat aplikasi terasa cepat. Pada jaringan yang lambat, ini seringkali membedakan antara “responsif” dan “apakah berhasil?”.
Pertrade-off-nya adalah penyimpangan data: apa yang dilihat pengguna bisa perlahan berhenti cocok dengan kebenaran di server. Drift biasanya muncul sebagai inkonsistensi kecil dan mengganggu yang bergantung pada waktu dan sulit direproduksi.
Pengguna cenderung memperhatikan drift ketika sesuatu “berubah pikiran” kemudian: sebuah counter melonjak lalu kembali, sebuah item muncul lalu hilang setelah refresh, edit tampak tersimpan sampai Anda kembali ke halaman, atau dua tab menampilkan nilai berbeda.
Ini terjadi karena UI membuat tebakan, dan server mungkin merespons dengan kebenaran yang berbeda. Aturan validasi, deduplikasi, pemeriksaan izin, batas laju, atau perangkat lain yang mengubah record yang sama dapat mengubah hasil akhir. Penyebab umum lain adalah permintaan yang saling tumpang tindih: respons lama tiba belakangan dan menimpa aksi terbaru pengguna.
Contoh: Anda mengganti nama proyek menjadi “Q1 Plan” dan langsung menampilkannya di header. Server mungkin memangkas spasi, menolak karakter, atau menghasilkan slug. Jika Anda tidak pernah mengganti nilai optimistis dengan nilai akhir dari server, UI akan terlihat benar sampai refresh berikutnya, ketika itu “secara misterius” berubah.
Optimistic UI tidak selalu pilihan yang tepat. Berhati-hatilah (atau hindari) untuk uang dan penagihan, aksi yang tidak bisa dibatalkan, perubahan peran dan izin, alur kerja dengan aturan server kompleks, atau apa pun yang memiliki efek samping yang harus dikonfirmasi pengguna.
Jika digunakan dengan baik, pembaruan optimistis membuat aplikasi terasa segera, tetapi hanya jika Anda merencanakan rekonsiliasi, pengurutan, dan penanganan kegagalan.
Optimistic UI bekerja paling baik ketika Anda memisahkan dua jenis state:
Sebagian besar drift dimulai saat tebakan lokal diperlakukan seolah-olah kebenaran yang sudah dikonfirmasi.
Aturan sederhana: jika sebuah nilai memiliki makna bisnis di luar layar saat ini, server adalah sumber kebenaran. Jika hanya memengaruhi cara layar berperilaku (terbuka atau tertutup, input fokus, teks draf), simpan secara lokal.
Dalam praktiknya, simpan kebenaran server untuk hal seperti izin, harga, saldo, inventaris, field terhitung atau tervalidasi, dan apa pun yang bisa berubah di tempat lain (tab lain, pengguna lain). Simpan state UI lokal untuk draf, flag "sedang mengedit", filter sementara, baris yang diperluas, dan toggle animasi.
Beberapa aksi “aman untuk ditebak” karena server hampir selalu menerimanya dan mudah dibatalkan, seperti memberi bintang pada item atau mengganti preferensi sederhana.
Ketika sebuah field tidak aman untuk ditebak, Anda masih bisa membuat aplikasi terasa cepat tanpa berpura-pura perubahan itu final. Simpan nilai yang terakhir dikonfirmasi, dan tambahkan sinyal pending yang jelas.
Contohnya, di layar CRM di mana Anda klik "Mark as paid", server mungkin menolak (izin, validasi, sudah dikembalikan). Alih-alih langsung menulis ulang setiap angka turunan, perbarui status dengan label "Saving..." yang halus, biarkan total tidak berubah, dan hanya perbarui total setelah konfirmasi.
Pola yang baik sederhana dan konsisten: lencana kecil "Saving..." di dekat item yang diubah, menonaktifkan sementara aksi (atau mengubahnya menjadi Undo) sampai permintaan selesai, atau menandai nilai optimistis sebagai sementara (teks lebih pudar atau spinner kecil).
Jika respons server dapat memengaruhi banyak tempat (total, pengurutan, field terhitung, izin), refetch biasanya lebih aman daripada mencoba mempatch semuanya. Jika itu perubahan kecil dan terisolasi (mengganti nama catatan, mengganti flag), mempatch secara lokal seringkali cukup.
Aturan yang berguna: patch satu hal yang diubah pengguna, lalu refetch data yang bersifat turunan, agregat, atau dibagikan antar layar.
Optimistic UI bekerja saat model data Anda melacak apa yang sudah dikonfirmasi versus apa yang masih tebakan. Jika Anda memodelkan celah itu secara eksplisit, momen "kenapa ini berubah kembali?" menjadi jarang.
Untuk item yang baru dibuat, beri ID klien sementara (mis. temp_12345 atau UUID), lalu ganti dengan ID server nyata saat respons datang. Itu memungkinkan list, seleksi, dan state pengeditan ber-rekonsiliasi dengan rapi.
Contoh: pengguna menambahkan tugas. Anda merendernya segera dengan id: "temp_a1". Ketika server merespons dengan id: 981, Anda mengganti ID di satu tempat, dan apa pun yang di-key oleh ID tetap berfungsi.
Flag loading tingkat layar terlalu kasar. Lacak status pada item (atau bahkan field) yang berubah. Dengan begitu Anda bisa menunjukkan UI pending yang halus, hanya meretry apa yang gagal, dan menghindari memblokir aksi yang tidak terkait.
Bentuk item yang praktis:
id: nyata atau sementarastatus: pending | confirmed | failedoptimisticPatch: apa yang Anda ubah secara lokal (kecil dan spesifik)serverValue: data yang terakhir dikonfirmasi (atau confirmedAt timestamp)rollbackSnapshot: nilai terkonfirmasi sebelumnya yang bisa Anda pulihkanPembaruan optimistis paling aman saat Anda hanya menyentuh apa yang benar-benar diubah pengguna (mis. mengganti completed) daripada mengganti seluruh objek dengan tebakan "versi baru." Penggantian seluruh objek memudahkan untuk menimpa edit yang lebih baru, field yang ditambahkan server, atau perubahan konkuren.
Pembaruan optimistis yang baik terasa instan, tapi tetap berakhir cocok dengan apa yang dikatakan server. Perlakukan perubahan optimistis sebagai sementara, dan simpan cukup banyak bookkeeping untuk mengonfirmasi atau membatalkannya dengan aman.
Contoh: pengguna mengedit judul tugas di daftar. Anda ingin judul langsung berubah, tapi Anda juga perlu menangani error validasi dan pemformatan sisi server.
Terapkan perubahan optimistis segera di state lokal. Simpan patch kecil (atau snapshot) agar Anda bisa mengembalikan.
Kirim permintaan dengan request ID (angka increment atau ID acak). Ini untuk mencocokkan respons dengan aksi yang memicunya.
Tandai item sebagai pending. Pending tidak harus memblokir UI. Bisa berupa spinner kecil, teks pudar, atau "Saving...". Intinya pengguna mengerti itu belum dikonfirmasi.
Saat berhasil, ganti data klien sementara dengan versi server. Jika server mengubah apa pun (memangkas spasi, mengubah kapitalisasi, memperbarui timestamp), perbarui state lokal agar cocok.
Saat gagal, kembalikan hanya apa yang diubah oleh permintaan ini dan tunjukkan error lokal yang jelas. Hindari mengembalikan bagian layar yang tidak terkait.
Berikut bentuk kecil yang bisa Anda ikuti (tanpa ketergantungan library):
const requestId = crypto.randomUUID();
applyOptimistic({ id, title: nextTitle, pending: requestId });
try {
const serverItem = await api.updateTask({ id, title: nextTitle, requestId });
confirmSuccess({ id, requestId, serverItem });
} catch (err) {
rollback({ id, requestId });
showError("Could not save. Your change was undone.");
}
Dua detail mencegah banyak bug: simpan request ID pada item saat pending, dan hanya konfirmasi atau rollback jika ID cocok. Itu menghentikan respons lama menimpa edit yang lebih baru.
Optimistic UI runtuh ketika jaringan membalas tidak berurutan. Kegagalan klasik: pengguna mengedit judul, mengedit lagi segera, dan permintaan pertama selesai terakhir. Jika Anda menerapkan respons lama itu, UI terpental kembali ke nilai lama.
Perbaikannya adalah memperlakukan setiap respons sebagai "mungkin relevan" dan menerapkannya hanya jika sesuai dengan intent pengguna terbaru.
Salah satu pola praktis adalah request ID klien (counter) yang dilampirkan ke setiap perubahan optimistis. Simpan ID terbaru per record. Saat respons datang, bandingkan ID. Jika respons lebih tua dari yang terbaru, abaikan.
Pemeriksaan versi juga membantu. Jika server mengembalikan updatedAt, version, atau etag, terima respons hanya jika lebih baru daripada apa yang sudah ditampilkan UI.
Opsi lain yang bisa Anda gabungkan:
Contoh (guard request ID):
let nextId = 1;
const latestByItem = new Map();
async function saveTitle(itemId, title) {
const requestId = nextId++;
latestByItem.set(itemId, requestId);
// optimistic update
setItems(prev => prev.map(i => i.id === itemId ? { ...i, title } : i));
const res = await api.updateItem(itemId, { title, requestId });
// ignore stale response
if (latestByItem.get(itemId) !== requestId) return;
// reconcile with server truth
setItems(prev => prev.map(i => i.id === itemId ? { ...i, ...res.item } : i));
}
Jika pengguna bisa mengetik cepat (catatan, judul, pencarian), pertimbangkan membatalkan atau menunda penyimpanan sampai mereka berhenti mengetik. Itu mengurangi beban server dan menurunkan kemungkinan respons terlambat menyebabkan tampilan terpental.
Kegagalan adalah tempat optimistis UI bisa kehilangan kepercayaan. Pengalaman terburuk adalah rollback tiba-tiba tanpa penjelasan.
Default yang baik untuk edit: biarkan nilai pengguna tetap di layar, tandai sebagai belum disimpan, dan tunjukkan error inline tepat di tempat mereka mengedit. Jika seseorang mengganti nama proyek dari “Alpha” menjadi “Q1 Launch,” jangan kembalikan ke “Alpha” kecuali Anda harus. Biarkan “Q1 Launch,” tambahkan “Not saved. Name already taken,” dan biarkan mereka memperbaikinya.
Umpan balik inline tetap terikat pada field atau baris yang gagal. Ini menghindari momen "apa yang baru saja terjadi?" di mana toast muncul tetapi UI diam-diam berubah kembali.
Petunjuk yang dapat diandalkan termasuk "Saving..." saat sedang berjalan, "Not saved" saat gagal, highlight halus pada baris yang terkena, dan pesan singkat yang memberi tahu pengguna apa yang harus dilakukan selanjutnya.
Retry hampir selalu membantu. Undo terbaik untuk aksi cepat yang mungkin disesalkan orang (seperti arsip), tetapi bisa membingungkan untuk edit di mana pengguna jelas menginginkan nilai baru.
Ketika mutasi gagal:
Jika Anda harus rollback (mis. izin berubah dan pengguna tidak bisa mengedit lagi), jelaskan dan pulihkan kebenaran server: "Couldn’t save. You no longer have access to edit this.".
Anggap respons server sebagai tanda terima, bukan hanya flag sukses. Setelah permintaan selesai, rekonsiliasi: pertahankan apa yang dimaksud pengguna, dan terima apa yang server lebih tahu.
Refetch penuh paling aman ketika server mungkin mengubah lebih dari tebakan lokal Anda. Ini juga lebih mudah dipahami.
Refetch biasanya pilihan lebih baik ketika mutasi memengaruhi banyak record (memindahkan item antar daftar), ketika izin atau aturan alur kerja bisa mengubah hasil, ketika server mengembalikan data parsial, atau ketika klien lain sering memperbarui tampilan yang sama.
Jika server mengembalikan entitas yang diperbarui (atau field yang cukup), merging bisa memberikan pengalaman lebih baik: UI tetap stabil sementara masih menerima kebenaran server.
Drift sering muncul dari menimpa field milik server dengan objek optimistis. Pikirkan counter, nilai terhitung, timestamp, dan pemformatan normal.
Contoh: Anda optimistis mengatur likedByMe=true dan menambah likeCount. Server mungkin mendeduplikasi double-like dan mengembalikan likeCount yang berbeda, plus updatedAt yang diperbarui.
Pendekatan merge sederhana:
Saat ada konflik, putuskan di muka. "Last write wins" cukup untuk toggle. Merge tingkat field lebih baik untuk form.
Melacak flag per-field "dirty since request" (atau nomor versi lokal) memungkinkan Anda mengabaikan nilai server untuk field yang diubah pengguna setelah mutasi dimulai, sambil tetap menerima kebenaran server untuk sisanya.
Jika server menolak mutasi, lebih suka error spesifik dan ringan daripada rollback mengejutkan. Pertahankan input pengguna, highlight field, dan tampilkan pesan. Simpan rollback untuk kasus di mana aksi benar-benar tidak bisa berdiri (mis. Anda secara optimistis menghapus item yang server tolak untuk dihapus).
Daftar adalah tempat optimistic UI terasa hebat dan mudah rusak. Satu item berubah dapat memengaruhi pengurutan, total, filter, dan banyak halaman.
Untuk creates, tampilkan item baru segera tetapi tandai sebagai pending, dengan ID sementara. Jaga posisinya stabil agar tidak lompat-lompat.
Untuk deletes, pola aman adalah menyembunyikan item segera tetapi menyimpan "ghost" record singkat di memori sampai server mengonfirmasi. Itu mendukung undo dan membuat kegagalan lebih mudah ditangani.
Pengurutan ulang sulit karena menyentuh banyak item. Jika Anda optimistis mengurutkan ulang, simpan urutan sebelumnya agar bisa mengembalikannya jika perlu.
Dengan paginasi atau infinite scroll, tentukan di mana sisipan optimistis berada. Di feed, item baru biasanya masuk di atas. Di katalog yang diurutkan oleh server, penyisipan lokal bisa menyesatkan karena server mungkin menempatkan item di tempat lain. Kompromi praktis adalah menyisipkan ke daftar yang terlihat dengan lencana pending, lalu siap memindahkannya setelah respons server jika kunci sort final berbeda.
Saat ID sementara menjadi ID nyata, deduplikasi dengan kunci stabil. Jika Anda hanya mencocokkan berdasarkan ID, Anda bisa menampilkan item yang sama dua kali (sementara dan terkonfirmasi). Simpan pemetaan tempId-ke-realId dan ganti di tempat agar posisi scroll dan seleksi tidak reset.
Jumlah dan filter juga adalah state daftar. Perbarui jumlah secara optimistis hanya ketika Anda yakin server akan setuju. Jika tidak, tandai sebagai sedang menyegarkan dan rekonsiliasi setelah respons.
Sebagian besar bug pembaruan optimistis bukan soal React. Mereka muncul dari memperlakukan perubahan optimistis sebagai "kebenaran baru" alih-alih tebakan sementara.
Memperbarui seluruh objek atau layar secara optimistis saat hanya satu field berubah memperlebar area dampak. Koreksi server kemudian dapat menimpa edit yang tidak terkait.
Contoh: form profil mengganti seluruh objek user saat Anda mengganti sebuah pengaturan. Saat permintaan sedang berjalan, pengguna mengedit nama mereka. Ketika respons datang, penggantian Anda bisa mengembalikan nama lama.
Jaga patch optimistis kecil dan terfokus.
Sumber drift lain adalah lupa menghapus flag pending setelah sukses atau error. UI tetap setengah-memuat, dan logika nanti mungkin menganggapnya masih optimistis.
Jika Anda melacak state pending per item, hapus menggunakan kunci yang sama saat Anda menyetelnya. ID sementara sering menyebabkan item "pending hantu" ketika ID nyata tidak dipetakan di mana-mana.
Bug rollback terjadi ketika snapshot disimpan terlambat atau cakupannya terlalu luas.
Jika pengguna membuat dua edit cepat, Anda bisa berakhir mengembalikan edit #2 menggunakan snapshot dari sebelum edit #1. UI melompat ke state yang pengguna tidak pernah lihat.
Perbaikan: snapshot bagian yang tepat yang akan Anda pulihkan, dan cakupkan ke percobaan mutasi spesifik (sering menggunakan request ID).
Simpan nyata sering multi-langkah. Jika langkah 2 gagal (mis. unggah gambar), jangan diam-diam membatalkan langkah 1. Tunjukkan apa yang tersimpan, apa yang tidak, dan apa yang bisa dilakukan pengguna selanjutnya.
Juga, jangan menganggap server akan mengembalikan persis apa yang Anda kirim. Server menormalkan teks, menerapkan izin, menetapkan timestamp, memberi ID, dan menghapus field. Selalu rekonsiliasi dari respons (atau refetch) daripada mempercayai patch optimistis selamanya.
Optimistic UI bekerja ketika dapat diprediksi. Perlakukan setiap perubahan optimistis seperti mini-transaksi: ia punya ID, state pending yang terlihat, swap sukses yang jelas, dan jalur kegagalan yang tidak mengejutkan orang.
Checklist yang harus ditinjau sebelum rilis:
Jika Anda membuat prototipe cepat, jaga versi pertama kecil: satu layar, satu mutasi, satu pembaruan daftar. Alat seperti Koder.ai dapat membantu Anda merancang UI dan API lebih cepat, tetapi aturan yang sama tetap berlaku: modelkan pending vs confirmed state sehingga klien tidak pernah kehilangan jejak apa yang sebenarnya diterima server.
Optimistic UI memperbarui tampilan segera, sebelum server mengonfirmasi perubahan. Ini membuat aplikasi terasa instan, tetapi Anda tetap harus merekonsiliasi dengan respons server agar UI tidak menyimpang dari state yang benar di server.
Penyimpangan data terjadi ketika UI mempertahankan tebakan optimistis seolah-olah sudah dikonfirmasi, namun server menyimpan sesuatu yang berbeda atau menolak perubahan tersebut. Biasanya terlihat setelah refresh, di tab lain, atau saat jaringan lambat membuat respons datang tidak berurutan.
Hindari atau sangat berhati-hati menggunakan pembaruan optimistis untuk uang, penagihan, tindakan yang tidak bisa dibatalkan, perubahan izin, dan alur kerja dengan aturan server yang ketat. Untuk kasus ini, lebih aman menampilkan status pending yang jelas dan menunggu konfirmasi sebelum mengubah hal-hal yang memengaruhi total atau akses.
Anggap backend sebagai sumber kebenaran untuk apa pun yang memiliki makna bisnis di luar layar saat ini, seperti harga, izin, field terhitung, dan counter bersama. Simpan state UI lokal untuk draf, fokus, "sedang mengedit", filter, dan state presentasional lain.
Tunjukkan sinyal kecil dan konsisten tepat di tempat perubahan terjadi, seperti “Saving…”, teks memudar, atau spinner halus. Tujuannya agar jelas bahwa nilai tersebut bersifat sementara tanpa memblokir seluruh halaman.
Gunakan ID klien sementara (seperti UUID atau temp_...) saat membuat item, lalu ganti dengan ID server yang asli setelah sukses. Ini menjaga key daftar, pemilihan, dan state pengeditan tetap stabil agar item tidak berkedip atau terduplikasi.
Jangan gunakan satu flag loading global; lacak state pending per item (atau per field) sehingga hanya bagian yang berubah yang tampak pending. Simpan patch optimistis kecil dan snapshot rollback sehingga Anda bisa mengonfirmasi atau membatalkan hanya perubahan itu tanpa memengaruhi UI yang tidak terkait.
Lampirkan request ID ke setiap mutasi dan simpan request ID terbaru per item. Saat respons tiba, terapkan hanya jika cocok dengan request ID terbaru; jika tidak cocok, abaikan sehingga respons terlambat tidak bisa mengembalikan UI ke nilai lama.
Untuk sebagian besar edit, biarkan nilai pengguna tetap terlihat, tandai sebagai belum tersimpan, dan tampilkan error inline di tempat mereka mengedit, dengan opsi Retry yang jelas. Lakukan rollback keras hanya ketika perubahan benar-benar tidak bisa diterima (mis. kehilangan izin), dan jelaskan alasannya.
Lakukan refetch ketika perubahan dapat memengaruhi banyak tempat seperti total, pengurutan, izin, atau field terderivasi, karena mempatch semuanya dengan benar mudah salah. Lakukan merge lokal ketika itu update kecil dan terisolasi dan server mengembalikan entitas yang diperbarui; kemudian bersihkan state pending dan terima field milik server seperti timestamp dan nilai terhitung.