Episode ini menguasai state management di sisi client: pola query, mutation, invalidate, dan optimistic updates, integrasi modern dengan @tanstack/react-query, serta teknik caching, refetch, dan penanganan stale data.

tRPC menjawab masalah tipe; React Query menjawab masalah server state. Saat keduanya digabung, kalian mendapat data fetching yang tipe-aman sekaligus caching, retry, dan sinkronisasi otomatis. Episode 8 membahas pola-pola inti: query, mutation, invalidate, optimistic updates, serta cara mengatur caching dan stale data.
Konsep ini penting karena di production, request yang sama dipanggil berkali-kali tanpa sadar — dan React Query menghematnya lewat cache.
Query hasilnya di-cache berdasarkan key yang diturunkan dari path dan input. Panggilan kedua ke procedure yang sama akan dijawab cache tanpa request baru:
function DaftarUser() {
const { data, isFetching, refetch } = trpc.user.list.useQuery(undefined, {
staleTime: 30_000,
});
if (!data) return <p>Memuat...</p>;
return (
<div>
<button onClick={() => refetch()}>Segarkan</button>
{isFetching && <span>Memperbarui di latar belakang</span>}
<ul>{data.map((u) => <li key={u.id}>{u.nama}</li>)}</ul>
</div>
);
}staleTime: 30_000 membuat data dianggap segar selama 30 detik — dalam jendela itu, remount komponen tidak memicu request ulang. refetch() memaksa pengambilan ulang, dan isFetching membedakan fetch awal dengan background refetch.
Setelah mutasi, cache data lama harus dibuang. Inilah peran invalidate:
function FormUser() {
const utils = trpc.useUtils();
const createUser = trpc.user.create.useMutation({
onSuccess: () => {
utils.user.list.invalidate();
},
});
return (
<button onClick={() => createUser.mutate({ nama: "Eka" })}>
Tambah user
</button>
);
}trpc.useUtils() (di v10 bernama useContext) memberikan akses ke cache. utils.user.list.invalidate() menandai query user.list sebagai basi, sehingga komponen yang memakainya otomatis refetch.
Optimistic update membuat UI terasa instan: tampilkan hasil seolah-olah mutasi sudah sukses, lalu rollback jika server menolak:
const deleteUser = trpc.user.delete.useMutation({
onMutate: async (deleted) => {
const utils = trpc.useUtils();
await utils.user.list.cancel();
const sebelumnya = utils.user.list.getData();
utils.user.list.setData(undefined, (lama) =>
(lama ?? []).filter((u) => u.id !== deleted.id),
);
return { sebelumnya };
},
onError: (_err, _vars, ctx) => {
if (ctx?.sebelumnya) {
utils.user.list.setData(undefined, ctx.sebelumnya);
}
},
onSettled: () => {
utils.user.list.invalidate();
},
});Alurnya: onMutate membatalkan query yang berjalan dan langsung mengubah cache dengan versi tanpa user yang dihapus. Jika gagal, onError mengembalikan data sebelumnya; jika selesai, onSettled memastikan cache selaras dengan server lewat invalidate.
Gunakan untuk operasi yang user-nya terasa cepat: toggle, hapus, dan update kecil. Untuk operasi dengan validasi kompleks atau risiko data salah, lebih aman menunggu response server dan menampilkan loading state.
Atur default di QueryClient agar konsisten di seluruh aplikasi:
import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000,
retry: 1,
refetchOnWindowFocus: false,
refetchOnMount: "always",
},
},
});Pilihan yang perlu kalian kenal:
staleTime: berapa lama data dianggap segar sebelum di-fetch ulang.refetchOnWindowFocus: apakah refetch saat tab kembali fokus — default true di React Query.retry: jumlah percobaan ulang otomatis saat gagal.gcTime: berapa lama cache tidak terpakai disimpan di memory.React Query membedakan status dengan jelas:
isPending: belum ada data sama sekali.isFetching: sedang mengambil data (termasuk background).isStale: data sudah kedaluwarsa dan butuh refetch.Pola praktik: tampilkan data lama sambil fetch ulang di latar belakang untuk UX yang mulus:
const { data, isFetching } = trpc.post.list.useQuery(undefined, {
staleTime: 5_000,
});
if (isFetching && data) return <p>Memperbarui...</p>;isFetching && data memungkinkan kalian memberi indikator tanpa mengosongkan layar — data yang sudah ada tetap ditampilkan.
Tip
Mulai dari staleTime kecil seperti 5 detik untuk data yang sering berubah, lalu naikkan untuk data statis. staleTime yang terlalu besar membuat user melihat data basi; terlalu kecil membuat request boros.
Episode 8 memberi kalian kendali penuh atas server state di client: query yang di-cache, invalidate untuk menjaga sinkronisasi, optimistic updates untuk UX yang responsif, serta pengaturan caching dan stale data yang sesuai kebutuhan aplikasi.
Inti yang harus dibawa pulang:
invalidate() menandai query basi agar di-fetch ulang.onMutate untuk optimistic update, onError untuk rollback.staleTime mengontrol umur data; refetchOnWindowFocus mengontrol refetch.isPending, isFetching, dan isStale adalah tiga status penting.Di episode 9 selanjutnya kita akan membahas schema evolution, versioning & backward compatibility — mengubah API tRPC tanpa memutus client, pendekatan versioning dan deprecation procedure, serta migrasi schema input dan output dengan menjaga kompatibilitas.