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.

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.
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:
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 selalu aman, tapi kadang lambat karena memicu request ulang. Alternatifnya: tulis langsung ke cache dengan setQueryData agar UI berubah instan:
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 update menampilkan hasil mutasi seketika, lalu membatalkannya jika server menolak. Kuncinya: simpan data lama di onMutate sebagai konteks, dan pulihkan di onError:
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.
List yang dimuat bertahap memakai useInfiniteQuery dengan getNextPageParam untuk menentukan cursor halaman berikutnya:
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.
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:
Di episode 6 selanjutnya kita akan membahas table core dan basic rendering — columnHelper untuk mendefinisikan kolom, useReactTable dengan getCoreRowModel, render lewat flexRender, serta fitur dasar sorting, filtering, dan pagination. Kalian akan membangun tabel pertama dengan TanStack Table!