Episode ini mengajarkan optimistic UI: memperbarui cache sebelum mutasi selesai dengan onMutate dan setQueryData, lalu rollback otomatis di onError. Kalian juga melihat use case nyata seperti like, toggle status, dan form inline.

Ketika user menekan tombol like, menunggu setengah detik lalu melihat ikon berubah terasa lambat. Pengalaman terbaik: ikon berubah seketika, dan jika server menolak, perubahan ditarik kembali dengan halus. Inilah optimistic update — mempercayai bahwa operasi akan berhasil, memperbarui UI lebih dulu, dan memperbaiki diri jika ternyata gagal.
Episode 11 membedah pola optimistic update di TanStack Query: update cache di onMutate, simpan data lama untuk rollback, dan kembalikan data saat onError terjadi.
Optimistic update mengubah urutan operasi: UI berubah sebelum server menjawab. TanStack Query mendukungnya lewat tiga callback useMutation:
onMutate → update cache lebih dulu (prediksi sukses)
mutationFn → kirim ke server
onError → rollback cache ke data lama
onSettled → invalidasi / refetch untuk konsistensi finalDiagram onMutate → update cache lebih dulu menunjukkan inti pola ini: UI diperbarui sebelum mutationFn selesai. onError menyediakan jalur kembali jika server menolak, dan onSettled membersihkan keadaan terakhir.
Contoh paling klasik: toggle status selesai pada todo. Ketika user mencentang, UI langsung berubah; jika server gagal, centang ditarik kembali:
import { useMutation, useQueryClient } from "@tanstack/react-query"
function TodoItem({ todo }) {
const queryClient = useQueryClient()
const toggle = useMutation({
mutationFn: (newTodo) =>
fetch(`https://jsonplaceholder.typicode.com/todos/${todo.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newTodo),
}).then((res) => res.json()),
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ["todos"] })
const previous = queryClient.getQueryData(["todos"])
queryClient.setQueryData(["todos"], (old) =>
old.map((t) => (t.id === newTodo.id ? newTodo : t))
)
return { previous }
},
onError: (error, _newTodo, context) => {
if (context?.previous) {
queryClient.setQueryData(["todos"], context.previous)
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] })
},
})
return (
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggle.mutate({ ...todo, completed: !todo.completed })}
/>
{todo.title}
</label>
)
}Alurnya berlapis. onMutate membatalkan query yang sedang berjalan agar tidak menimpa update kita, menyimpan previous ke context, lalu setQueryData langsung mengganti status todo. onError mengembalikan previous ke cache, dan onSettled meng-invalidate agar data akhir selalu sesuai server. context adalah nilai yang dikembalikan dari onMutate dan diteruskan ke onError dan onSettled.
cancelQueries membatalkan refetch yang sedang berjalan. Tanpa ini, refetch background bisa menimpa update optimistic dengan data lama yang belum mencerminkan perubahan. await queryClient.cancelQueries({ queryKey: ["todos"] }) memastikan cache dalam keadaan tenang sebelum kita menulis data baru.
Interaksi seperti like, bookmark, dan toggle adalah kandidat sempurna: operasinya cepat, data hasilnya pasti, dan feedback instan sangat dirasakan user. Pola di atas langsung bisa diterapkan.
Form yang bisa diedit langsung di tempat — rename judul, ubah deskripsi — juga cocok. UI menampilkan nilai baru seketika, dan rollback hanya terjadi jika validasi server menolak:
const rename = useMutation({
mutationFn: updateTitle,
onMutate: async ({ id, title }) => {
await queryClient.cancelQueries({ queryKey: ["todos"] })
const previous = queryClient.getQueryData(["todos"])
queryClient.setQueryData(["todos"], (old) =>
old.map((t) => (t.id === id ? { ...t, title } : t))
)
return { previous }
},
onError: (error, _vars, context) => {
if (context?.previous) {
queryClient.setQueryData(["todos"], context.previous)
}
},
})Untuk reorder item, optimistic update membuat posisi item berubah seketika saat drag selesai, lalu server dikirimi urutan baru. Jika gagal, urutan dikembalikan ke posisi semula.
Kunci keandalan rollback adalah context: nilai yang dikembalikan onMutate disimpan, lalu dipakai untuk mengembalikan cache di onError. Jangan pernah menyimpan data lama di closure luar — context menjamin data yang dikembalikan tepat untuk mutation yang gagal.
Optimistic update tetap harus diakhiri dengan konsistensi. onSettled berjalan baik sukses maupun gagal, dan invalidateQueries di sana memastikan cache akhir selalu mencerminkan server — sekaligus menutup kemungkinan perbedaan kecil antara prediksi dan kenyataan.
Warning
Optimistic update bukan untuk semua mutasi. Untuk operasi yang hasilnya tidak bisa diprediksi dengan pasti — pembayaran, upload file, atau transformasi data di server — lebih baik tampilkan loading biasa daripada menampilkan data yang ternyata salah.
Episode 11 memberi kalian pola optimistic UI yang lengkap: update cache di onMutate, simpan data lama di context, rollback di onError, dan konsistensi final di onSettled. Kalian juga tahu use case mana yang cocok dan mana yang sebaiknya dihindari.
Inti yang harus dibawa pulang:
onMutate memperbarui cache sebelum mutationFn selesai.cancelQueries mencegah refetch menimpa update optimistic.context untuk rollback yang akurat.onError mengembalikan cache ke data sebelumnya.onSettled dengan invalidateQueries menjamin konsistensi akhir.Di episode 12 selanjutnya kita akan membahas persistence dan offline — menyimpan cache ke localStorage atau IndexedDB dengan persister, serta perilaku offline dengan onlineManager dan retry exponential backoff. Kalian siap membangun aplikasi yang tangguh di jaringan buruk.