React Query optimistic update pattern
Optimistic update di React Query — UI langsung berubah saat mutate, rollback otomatis kalau request fail. Feel instant kayak native app.
Dipublikasikan 10 Juni 2026
Klik like di Tokopedia, tombol langsung jadi merah. Itu optimistic update. Tanpa pattern ini, user nunggu 500ms-2 detik dan UX kerasa lemot. React Query (TanStack Query) handle rollback otomatis kalau server tolak. Snippet ini implementasi like / unlike produk dengan proper invalidation.
Kode
import { QueryClient, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
interface Produk {
id: number;
nama: string;
liked: boolean;
jumlah_like: number;
}
// API client
const api = {
async fetchProduk(id: number): Promise<Produk> {
const r = await fetch(`/api/produk/${id}`);
if (!r.ok) throw new Error("Fetch fail");
return r.json();
},
async toggleLike(id: number, liked: boolean): Promise<Produk> {
const r = await fetch(`/api/produk/${id}/like`, {
method: liked ? "POST" : "DELETE",
});
if (!r.ok) throw new Error("Toggle fail");
return r.json();
},
};
// Hook query produk
export function useProduk(id: number) {
return useQuery({
queryKey: ["produk", id],
queryFn: () => api.fetchProduk(id),
staleTime: 30_000,
});
}
// Hook mutasi dengan optimistic update
export function useToggleLike(id: number) {
const qc = useQueryClient();
const key = ["produk", id];
return useMutation({
mutationFn: (liked: boolean) => api.toggleLike(id, liked),
onMutate: async (liked) => {
// 1. Cancel query inflight supaya gak overwrite optimistic state
await qc.cancelQueries({ queryKey: key });
// 2. Snapshot state sebelum diubah
const previous = qc.getQueryData<Produk>(key);
// 3. Optimistic update — bikin UI langsung berubah
if (previous) {
qc.setQueryData<Produk>(key, {
...previous,
liked,
jumlah_like: previous.jumlah_like + (liked ? 1 : -1),
});
}
// 4. Return context buat rollback
return { previous };
},
onError: (_err, _vars, context) => {
// Rollback kalau server tolak
if (context?.previous) {
qc.setQueryData(key, context.previous);
}
},
onSettled: () => {
// Sync ulang dari server (sukses maupun fail)
qc.invalidateQueries({ queryKey: key });
},
});
}
Pemakaian
// Component pakai hook
import { useProduk, useToggleLike } from "./useProduk";
export function TombolLike({ produkId }: { produkId: number }) {
const { data: produk, isPending } = useProduk(produkId);
const toggleLike = useToggleLike(produkId);
if (isPending || !produk) return <span>Loading...</span>;
return (
<button
onClick={() => toggleLike.mutate(!produk.liked)}
aria-pressed={produk.liked}
>
{produk.liked ? "Liked" : "Like"} ({produk.jumlah_like})
</button>
);
}
// Pattern untuk list — update item di tengah list
function useToggleLikeInList(produkId: number, listKey: unknown[]) {
const qc = useQueryClient();
return useMutation({
mutationFn: (liked: boolean) => api.toggleLike(produkId, liked),
onMutate: async (liked) => {
await qc.cancelQueries({ queryKey: listKey });
const previous = qc.getQueryData<Produk[]>(listKey);
qc.setQueryData<Produk[]>(listKey, (old) =>
old?.map((p) =>
p.id === produkId
? { ...p, liked, jumlah_like: p.jumlah_like + (liked ? 1 : -1) }
: p
)
);
return { previous };
},
onError: (_err, _vars, ctx) => {
if (ctx?.previous) qc.setQueryData(listKey, ctx.previous);
},
onSettled: () => qc.invalidateQueries({ queryKey: listKey }),
});
}
Kapan dipakai
- Like / favorite / bookmark button (Tokopedia, Bukalapak style).
- Toggle done di todo list.
- Update quantity di cart e-commerce.
- Inline edit field (status order, label tag).
- Drag & drop reorder list.
Catatan
- cancelQueries dulu — kalau ada fetch inflight saat optimistic, response yang datang kemudian akan overwrite state optimistic kamu. Cancel dulu.
- Snapshot di onMutate — kalau lupa, rollback impossible. Context-nya hilang.
- invalidateQueries di onSettled — selalu re-sync dari server, jangan trust optimistic state forever. Server bisa punya counter berbeda (race dengan user lain).
- Jangan optimistic untuk action irreversible — delete, transfer dana, kirim email. Pakai loading state biasa.
- Stale data leak — kalau component unmount sebelum onSettled jalan, query cache bisa stuck di optimistic state untuk component lain. Always invalidate.
useMutation context typing tricky. Define explicit type di generic:
useMutation<Produk, Error, boolean, { previous?: Produk }>. Auto-infer kadang kasihanydan kamu kehilangan type safety.
# tags
react-querytanstack-queryoptimisticuxreact
Ditulis oleh Asti Larasati · 10 Juni 2026