Belajar TanStack - Data Mutations & Optimistic Updates
Episode 5 of 24

Belajar TanStack - Data Mutations & Optimistic Updates

Episode ini membedah mutasi data dengan useMutation, invalidasi query setelah mutasi, optimistic updates dengan rollback saat gagal, serta paginated data dan infinite scrolling dengan useInfiniteQuery.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Pendahuluan

Episode 4 membahas membaca data. Tapi aplikasi nyata juga harus menulis: membuat todo, mengupdate profil, menghapus item. Di sinilah useMutation berperan — pasangan alami dari useQuery yang mengubah data di server lalu menyinkronkan cache.

Episode 5 membedah useMutation, invalidasi query, optimistic updates dengan rollback, serta pola paginated data dan infinite scrolling lewat useInfiniteQuery. Semua pola ini muncul di hampir setiap aplikasi produksi.

Pada akhir episode, kalian akan mampu membangun UI yang terasa instan: menekan tombol, data langsung berubah di layar, lalu diperbaiki diam-diam dengan hasil server.

Dasar useMutation

Mutasi Data dan Invalidasi Query

useMutation mirip useQuery, tapi untuk operasi yang mengubah data — POST, PUT, PATCH, dan DELETE. Setelah sukses, query yang terdampak harus di-invalidate agar data terbaru di-fetch:

JSMutasi dengan useMutation
import { useMutation, useQueryClient } from "@tanstack/react-query"
 
function TambahTodo() {
  const queryClient = useQueryClient()
 
  const mutation = useMutation({
    mutationFn: (judul) =>
      fetch("/api/todos", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ judul }),
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["todos"] })
    },
  })
 
  return <button onClick={() => mutation.mutate("Tugas baru")}>Tambah</button>
}

mutationFn menerima variabel — di sini judul. Saat berhasil, onSuccess memanggil invalidateQueries({ queryKey: ["todos"] }) yang menandai query todos sebagai basi dan memicu refetch.

Invalidasi dan Cache Update

Setelah Mutasi: Refetch atau Tulis Langsung

Invalidasi selalu aman, tapi kadang lambat karena memicu request ulang. Alternatifnya: tulis langsung ke cache dengan setQueryData agar UI berubah instan:

JSUpdate cache langsung tanpa refetch
const mutation = useMutation({
  mutationFn: tambahTodo,
  onSuccess: (todoBaru) => {
    queryClient.setQueryData(["todos"], (lama) => [...lama, todoBaru])
  },
})

setQueryData(["todos"], (lama) => [...lama, todoBaru]) menambahkan item baru ke array cache tanpa menunggu request ulang. Untuk operasi yang hasilnya mudah diketahui, pola ini lebih cepat; untuk yang hasilnya tidak pasti, tetap pakai invalidasi.

Optimistic Updates dan Rollback

Mengubah UI Sebelum Server Menjawab

Optimistic update menampilkan hasil mutasi seketika, lalu membatalkannya jika server menolak. Kuncinya: simpan data lama di onMutate sebagai konteks, dan pulihkan di onError:

JSOptimistic update dengan rollback
const mutation = useMutation({
  mutationFn: perbaruiTodo,
  onMutate: async (todoBaru) => {
    await queryClient.cancelQueries({ queryKey: ["todos"] })
    const sebelumnya = queryClient.getQueryData(["todos"])
    queryClient.setQueryData(["todos"], (lama) =>
      lama.map((t) => (t.id === todoBaru.id ? todoBaru : t))
    )
    return { sebelumnya }
  },
  onError: (err, todoBaru, konteks) => {
    queryClient.setQueryData(["todos"], konteks.sebelumnya)
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ["todos"] })
  },
})

onMutate membatalkan query yang sedang berjalan, menyimpan data lama, lalu menulis versi optimis. Jika gagal, onError memulihkan konteks.sebelumnya. Terakhir, onSettled menjalankan invalidasi untuk memastikan konsistensi dengan server.

Paginated Data dan Infinite Scrolling

useInfiniteQuery untuk Daftar Tanpa Akhir

List yang dimuat bertahap memakai useInfiniteQuery dengan getNextPageParam untuk menentukan cursor halaman berikutnya:

JSuseInfiniteQuery untuk infinite scroll
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ["proyek"],
  queryFn: ({ pageParam }) => ambilProyek(pageParam),
  initialPageParam: 1,
  getNextPageParam: (halamanTerakhir) => halamanTerakhir.nextCursor ?? undefined,
})
 
return (
  <div onScroll={handleScroll}>
    {data.pages.flatMap((p) => p.items).map((item) => <Item key={item.id} item={item} />)}
    <button onClick={() => fetchNextPage()} disabled={!hasNextPage}>
      Muat lagi
    </button>
  </div>
)

pageParam disuntikkan otomatis ke queryFn mulai dari initialPageParam: 1. getNextPageParam membaca cursor dari halaman terakhir; ketika mengembalikan undefined, hasNextPage menjadi false dan tombol nonaktif.

Warning

Saat menulis optimistic update, selalu cancel query yang sedang berjalan di onMutate. Kalau tidak, data lama dari fetch yang masih berjalan bisa menimpa data optimis kalian.

Penutup

Episode 5 melengkapi sisi tulis dari TanStack Query: useMutation untuk mengubah data, invalidasi dan setQueryData untuk menyinkronkan cache, optimistic updates dengan rollback yang aman, serta useInfiniteQuery untuk daftar tak terbatas.

Inti yang harus dibawa pulang:

  • useMutation menangani operasi tulis dengan mutationFn.
  • Invalidasi query adalah cara paling aman menyinkronkan cache.
  • setQueryData menulis cache langsung agar UI instan.
  • Optimistic update butuh cancelQueries dan penyimpanan data lama.
  • onError memulihkan data lama; onSettled menjalankan invalidasi.
  • useInfiniteQuery memakai pageParam dan getNextPageParam.

Di episode 6 selanjutnya kita akan membahas table core dan basic renderingcolumnHelper untuk mendefinisikan kolom, useReactTable dengan getCoreRowModel, render lewat flexRender, serta fitur dasar sorting, filtering, dan pagination. Kalian akan membangun tabel pertama dengan TanStack Table!