Episode ini mengajarkan operasi tulis dengan useMutation: status isPending dan isError, callback onSuccess dan onError, serta dua pola sinkronisasi cache yaitu invalidateQueries dan setQueryData setelah mutasi berhasil.

Membaca data baru separuh perjalanan. Separuh lainnya adalah menulis data — menambah todo, mengubah profil, menghapus item. Di pola manual, operasi tulis juga berantakan: status loading manual, error yang mudah terlewat, dan cache yang tidak pernah diperbarui sehingga halaman menampilkan data basi.
Episode 6 membahas useMutation untuk operasi tulis, lalu dua pola sinkronisasi cache yang membuat UI selalu konsisten dengan server: invalidateQueries dan setQueryData.
useMutation hampir sama dengan useQuery, tapi tidak berjalan otomatis saat mount — dia menunggu dipanggil lewat method mutate atau mutateAsync:
import { useMutation, useQueryClient } from "@tanstack/react-query"
async function createTodo(title) {
const res = await fetch("https://jsonplaceholder.typicode.com/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, completed: false }),
})
if (!res.ok) throw new Error("Gagal membuat todo")
return res.json()
}
function TodoForm() {
const mutation = useMutation({ mutationFn: createTodo })
return (
<form
onSubmit={(event) => {
event.preventDefault()
const title = event.target.title.value
mutation.mutate(title)
}}
>
<input name="title" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Menyimpan..." : "Simpan"}
</button>
{mutation.isError && <p>Gagal: {mutation.error.message}</p>}
</form>
)
}mutation.mutate(title) memicu createTodo dengan argumen title. Status isPending menandakan operasi sedang berjalan dan cocok untuk mematikan tombol; isError dan error menampilkan kegagalan. useMutation tidak otomatis menulis ke cache — itu tugas kalian berikutnya.
Kalian bisa bereaksi terhadap keberhasilan atau kegagalan lewat callback yang didefinisikan saat mendeklarasikan mutation:
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: (data) => {
console.log("Todo baru:", data)
},
onError: (error) => {
console.error("Mutasi gagal:", error)
},
})onSuccess menerima hasil mutationFn sebagai argumen pertama. Di v5, callback ini hanya tersedia di useMutation, bukan di useQuery — perubahan penting yang harus diingat kalau kalian migrasi dari v4.
Setelah mutasi berhasil, data di cache mungkin sudah tidak mencerminkan server. Cara paling umum untuk memperbarui adalah invalidasi: tandai query tertentu sebagai basi sehingga dia di-refetch otomatis pada kesempatan berikutnya.
import { useMutation, useQueryClient } from "@tanstack/react-query"
function TodoForm() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] })
},
})
return <Form /> // markup di episode sebelumnya
}queryClient.invalidateQueries({ queryKey: ["todos"] }) menandai semua key berprefix ["todos"] sebagai basi. Karena key hierarkis, query ["todos", 5] dan ["todos", "detail", 5] ikut di-refetch jika sedang aktif. invalidateQueries adalah pola default yang direkomendasikan karena paling sederhana dan sulit salah.
Jika mutasi hanya menyentuh satu item, validasi cakupannya agar tidak menembak terlalu banyak query:
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos", id] })
}queryKey: ["todos", id] membidik item spesifik, bukan seluruh daftar. invalidateQueries menerima ekspresi key parsial — pemilihan cakupan ini bagian dari seni mengelola performa yang akan kita dalami di episode 13.
Untuk UI yang lebih responsif, kalian bisa menulis langsung ke cache dengan setQueryData tanpa menunggu refetch. Ini pola untuk data yang sudah diketahui bentuknya:
onSuccess: (newTodo) => {
queryClient.setQueryData(["todos"], (old) => [...(old ?? []), newTodo])
}queryClient.setQueryData(["todos"], ...) menggabungkan todo baru ke array yang ada. setQueryData menerima updater function seperti useState — argumen old adalah data cache saat ini, dan nilai kembaliannya menjadi data baru.
setQueryData untuk kecepatan, lalu invalidateQueries untuk memastikan konsistensi final.Warning
setQueryData menulis cache tanpa memvalidasi bentuk data. Pastikan bentuk data yang ditulis sama dengan yang diharapkan komponen lain, kalau tidak render akan membuang error aneh yang sulit ditelusuri.
Episode 6 melengkapi siklus baca-tulis: membaca dengan useQuery, menulis dengan useMutation, lalu menyinkronkan cache dengan invalidateQueries untuk refetch otomatis atau setQueryData untuk update langsung. Kalian sekarang bisa membangun aplikasi yang datanya selalu konsisten.
Inti yang harus dibawa pulang:
useMutation dipicu lewat mutate atau mutateAsync, bukan otomatis.isPending mengindikasikan operasi berjalan; isError untuk kegagalan.onSuccess dan onError tersedia di useMutation, bukan useQuery.invalidateQueries menandai query basi agar di-refetch otomatis.setQueryData menulis langsung ke cache untuk update instan.Di episode 7 selanjutnya kita akan membahas staleness dan refetching — perbedaan staleTime dan gcTime, serta opsi refetchOnWindowFocus, refetchOnReconnect, refetchInterval, dan refetch manual. Kalian akan paham kapan data di-refetch dan bagaimana mengendalikannya.