Episode ini membahas sisi penulisan data RTK Query: endpoints.mutation dan hook useXMutation untuk POST, PUT, dan DELETE, optimistic update via onQueryStarted, serta sinkronisasi cache dengan providesTags dan invalidatesTags agar daftar data selalu konsisten setelah mutasi.

Episode 8 hanya membahas membaca data. Padahal aplikasi nyata juga harus membuat, memperbarui, dan menghapus — dan setelah itu UI harus menampilkan data terbaru. Episode 9 menutup celah tersebut dengan mutations dan cache invalidation: dua mekanisme RTK Query yang menjaga server state tetap sinkron setelah operasi menulis.
Masalah yang sering muncul di aplikasi manual: setelah POST berhasil, daftar di layar tetap basi karena tidak ada trigger untuk mengulang fetch. RTK Query menyelesaikannya secara deklaratif. Kalian cukup menyatakan bahwa suatu query bergantung pada sebuah tag, lalu mutation akan menandai tag itu basi — query pun di-fetch ulang otomatis.
endpoints.mutation menangani operasi yang mengubah data di server: POST, PUT, PATCH, dan DELETE. Seperti query, mutation memakai query yang bisa menyesuaikan method dan body:
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
import type { Post } from "./postsApi"
export const postsApi = createApi({
reducerPath: "postsApi",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => "posts",
providesTags: (result) =>
result
? [...result.map(({ id }) => ({ type: "Post" as const, id })),
{ type: "Post" as const, id: "LIST" }]
: [{ type: "Post" as const, id: "LIST" }],
}),
addPost: builder.mutation<Post, Partial<Post>>({
query: (body) => ({
url: "posts",
method: "POST",
body,
}),
invalidatesTags: [{ type: "Post", id: "LIST" }],
}),
}),
})Perhatikan providesTags pada getPosts: hasil query mendaftarkan satu tag per post dengan id masing-masing, plus satu tag khusus "LIST". Sedangkan addPost mendeklarasikan invalidatesTags untuk "LIST". Artinya: setiap kali addPost berhasil, seluruh query yang menyediakan tag Post:LIST akan di-fetch ulang.
Dari endpoint mutation, RTK Query menghasilkan hook useAddPostMutation. Berbeda dengan query hook, mutation hook mengembalikan pasangan [trigger, result]:
import { useAddPostMutation } from "../services/postsApi"
export function CreatePost() {
const [addPost, { isLoading }] = useAddPostMutation()
const submit = async (title: string, body: string) => {
try {
await addPost({ title, body }).unwrap()
console.log("Post berhasil dibuat")
} catch (error) {
console.error("Gagal membuat post", error)
}
}
return <button disabled={isLoading} onClick={() => submit("Halo", "Isi")}>Kirim</button>
}addPost mengembalikan promise; memanggil .unwrap() mengubahnya menjadi promise biasa sehingga error bisa ditangkap dengan try/catch. Tanpa .unwrap(), error hanya tersimpan di result.
Pessimistic update menunggu respons server baru memperbarui cache. Optimistic update melakukan kebalikannya: cache langsung diperbarui dengan hasil bayangan, lalu dibatalkan bila server menolak. Keduanya ditulis di dalam onQueryStarted:
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
import type { Post } from "./postsApi"
updatePost: builder.mutation<Post, Post>({
query: ({ id, ...patch }) => ({
url: `posts/${id}`,
method: "PATCH",
body: patch,
}),
onQueryStarted: async ({ id, ...patch }, { dispatch, queryFulfilled }) => {
const patchResult = dispatch(
postsApi.util.updateQueryData("getPostById", id, (draft) => {
Object.assign(draft, patch)
}),
)
try {
await queryFulfilled
} catch {
patchResult.undo()
}
},
}),updateQueryData mengubah data cache dengan Immer draft. Bila queryFulfilled menolak (request gagal), patchResult.undo() mengembalikan cache ke kondisi sebelum optimasi. UI terasa cepat karena diperbarui sebelum server membalas.
Warning
Optimistic update terbaik untuk operasi yang hampir pasti berhasil, seperti like atau mengubah preferensi. Untuk operasi yang sering gagal, misalnya upload file besar, pessimistic update lebih aman karena tidak membuang data lokal.
Selain tag "LIST", kalian bisa membuat invalidation bergranular per item. Ini penting ketika satu post diupdate: hanya detail post itu yang di-fetch ulang, sementara daftar lain yang tidak menyentuh data itu tetap tersimpan:
updatePost: builder.mutation<Post, Partial<Post> & { id: number }>({
query: ({ id, ...patch }) => ({
url: `posts/${id}`,
method: "PATCH",
body: patch,
}),
invalidatesTags: (_result, _error, arg) => [{ type: "Post", id: arg.id }],
}),
deletePost: builder.mutation<{ success: boolean }, number>({
query: (id) => ({
url: `posts/${id}`,
method: "DELETE",
}),
invalidatesTags: (_result, _error, id) => [{ type: "Post", id }],
}),Ketika updatePost berhasil, hanya tag Post:{id} yang dibatalkan — bukan seluruh daftar. Query getPostById(id) akan di-fetch ulang, sedangkan getPosts tetap memakai cache lamanya. Kombinasi tag granular dan "LIST" memberikan keseimbangan antara akurasi dan efisiensi jaringan.
Ada kalanya daftar memang harus dimuat ulang seluruhnya setelah operasi menulis, misalnya setelah membuat item baru yang mengubah urutan. Triknya sederhana: tambahkan juga tag "LIST" pada invalidatesTags mutation tersebut:
addPost: builder.mutation<Post, Partial<Post>>({
query: (body) => ({ url: "posts", method: "POST", body }),
invalidatesTags: [
{ type: "Post", id: "LIST" },
],
}),Setelah addPost selesai, semua query yang providesTags menyertakan Post:LIST akan menandai cachenya basi dan memicu re-fetch. Pastikan setiap query yang bergantung pada daftar tersebut memang menyediakan tag yang sama, kalau tidak sinkronisasi tidak akan terjadi.
Mutations melengkapi RTK Query menjadi data fetching yang utuh: endpoints.mutation menangani operasi menulis, hook useXMutation menyediakan trigger dan status, onQueryStarted memungkinkan optimistic update yang bisa dibatalkan, dan tags menjadi kontrak deklaratif untuk invalidation. Hasilnya, konsistensi antara server dan UI dirawat otomatis tanpa kode sinkronisasi manual.
Inti yang harus dibawa pulang:
endpoints.mutation digunakan untuk POST, PUT, PATCH, dan DELETE..unwrap() untuk error handling.providesTags menyatakan query mana yang menyediakan data bernama tag.invalidatesTags menandai tag basi sehingga query terkait di-fetch ulang.onQueryStarted dengan updateQueryData memungkinkan optimistic update yang bisa di-undo."LIST" untuk invalidation yang efisien.Episode 10 berikutnya mengalihkan fokus ke normalisasi data — kalian akan belajar createEntityAdapter untuk mengelola koleksi dengan CRUD terotomatisasi, selector bawaan selectAll dan selectById, plus sorting, yang berguna untuk slice yang menyimpan banyak entitas.