Belajar Redux - RTK Query: Mutations & Cache Invalidation
Episode 9 of 23

Belajar Redux - RTK Query: Mutations & Cache Invalidation

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.

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

Pendahuluan

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.

Membuat Endpoint Mutation

Builder Mutation untuk Menulis

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:

JSEndpoint mutation pada postsApi
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.

Hook useXMutation

Dari endpoint mutation, RTK Query menghasilkan hook useAddPostMutation. Berbeda dengan query hook, mutation hook mengembalikan pasangan [trigger, result]:

JSMemakai useAddPostMutation
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.

Optimistic dan Pessimistic Update

Update Lokal Sebelum Konfirmasi Server

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:

JSOptimistic update dengan 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.

Invalidasi Cache yang Tepat

Tag Granular dengan ID

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:

JSInvalidasi granular per post
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.

Mengontrol Re-fetch Seluruh Daftar

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:

JSInvalidasi LIST setelah operasi menulis
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.

Penutup

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.
  • Hook mutation mengembalikan pasangan trigger dan result; gunakan .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.
  • Gabungkan tag granular per id dengan tag "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.