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

Learn TanStack - Data Mutations & Optimistic Updates

This episode dissects data mutations with useMutation, query invalidation after a mutation, optimistic updates with rollback on failure, and paginated data with infinite scrolling using useInfiniteQuery.

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

Introduction

Episode 4 covered reading data. But real apps also need to write: creating todos, updating profiles, deleting items. That's where useMutation comes in — the natural partner to useQuery that changes data on the server, then syncs the cache.

Episode 5 dissects useMutation, query invalidation, optimistic updates with rollback, and the paginated data and infinite scrolling patterns through useInfiniteQuery. These patterns appear in nearly every production application.

By the end of the episode, you'll be able to build UI that feels instant: press a button, data changes on screen right away, then gets quietly reconciled with the server's result.

useMutation Basics

Data Mutations and Query Invalidation

useMutation is similar to useQuery, but for operations that change data — POST, PUT, PATCH, and DELETE. After success, the affected queries must be invalidated so the latest data is fetched:

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 receives a variable — here judul. On success, onSuccess calls invalidateQueries({ queryKey: ["todos"] }), which marks the todos query stale and triggers a refetch.

Invalidation and Cache Update

After a Mutation: Refetch or Write Directly

Invalidation is always safe, but sometimes slow because it triggers another request. The alternative is writing to the cache directly with setQueryData so the UI changes instantly:

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

setQueryData(["todos"], (lama) => [...lama, todoBaru]) adds the new item to the cached array without waiting for another request. For operations whose result is easy to predict, this pattern is faster; for uncertain results, stick with invalidation.

Optimistic Updates and Rollback

Changing the UI Before the Server Responds

An optimistic update shows the mutation result immediately, then reverts it if the server rejects it. The key is saving the old data in onMutate as context and restoring it in 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 cancels any in-flight queries, saves the old data, then writes the optimistic version. If it fails, onError restores konteks.sebelumnya. Finally, onSettled runs the invalidation to guarantee consistency with the server.

Paginated Data and Infinite Scrolling

useInfiniteQuery for Endless Lists

Lists loaded incrementally use useInfiniteQuery with getNextPageParam to determine the cursor for the next page:

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 is injected into queryFn automatically, starting from initialPageParam: 1. getNextPageParam reads the cursor from the last page; when it returns undefined, hasNextPage becomes false and the button is disabled.

Warning

When writing an optimistic update, always cancel in-flight queries in onMutate. Otherwise, old data from a still-running fetch can overwrite your optimistic data.

Conclusion

Episode 5 completed the write side of TanStack Query: useMutation for changing data, invalidation and setQueryData for syncing the cache, optimistic updates with safe rollback, and useInfiniteQuery for endless lists.

Key takeaways:

  • useMutation handles write operations through mutationFn.
  • Query invalidation is the safest way to sync the cache.
  • setQueryData writes the cache directly for an instant UI.
  • Optimistic updates need cancelQueries and saving the old data.
  • onError restores old data; onSettled runs invalidation.
  • useInfiniteQuery uses pageParam and getNextPageParam.

In the next episode, episode 6, we'll discuss table core and basic renderingcolumnHelper for defining columns, useReactTable with getCoreRowModel, rendering through flexRender, and the basic sorting, filtering, and pagination features. You'll build your first table with TanStack Table!

Learn TanStack - Data Mutations & Optimistic Updates | Learn TanStack