Episode ini membahas pola hybrid: TanStack Query untuk server state dan Zustand atau Redux untuk UI state, serta memperlakukan cache sebagai single source of truth dengan getQueryData agar data tidak diduplikasi di dua tempat.

Kesalahan arsitektur yang paling umum di aplikasi React: menyimpan data server di store global. Developer menaruh hasil fetch ke Redux atau Zustand, lalu dua sistem saling bersaing mengelola data yang sama — TanStack Query sudah menyimpan cache, store juga menyimpan salinannya, dan keduanya bisa bertengkar.
Episode 16 memperkenalkan pola hybrid: TanStack Query mengelola server state, Zustand atau Redux mengelola UI state. Lalu kita membahas cara memperlakukan cache sebagai single source of truth.
Pembagian tugasnya tegas. TanStack Query menangani semua data yang berasal dari server — fetch, cache, retry, invalidasi. Store menangani state yang lahir dan mati di client:
TanStack Query → server state: todos, users, profil, notifikasi
Zustand/Redux → UI state: modal terbuka, theme, keranjang, filter aktifPembagian TanStack Query → server state dan Zustand/Redux → UI state membuat setiap sistem mengelola domain yang jelas. Tidak ada dua sumber kebenaran untuk data yang sama.
Zustand cocok untuk UI state yang ringan. Contoh keranjang yang menyimpan id produk (UI state) sementara data produk diambil dari TanStack Query:
import { create } from "zustand"
export const useCartStore = create((set) => ({
productIds: [],
cartOpen: false,
addProduct: (id) =>
set((state) => ({ productIds: [...state.productIds, id] })),
toggleCart: () => set((state) => ({ cartOpen: !state.cartOpen })),
}))useCartStore hanya menyimpan productIds dan cartOpen — bukan data produk itu sendiri. addProduct dan toggleCart memutasi UI state murni. Data detail produk tetap diambil lewat useQuery di komponen.
Komponen memakai kedua sumber tanpa konflik: store untuk kontrol UI, query untuk data:
function CartButton() {
const { productIds, cartOpen, toggleCart } = useCartStore()
const { data: products } = useQuery({
queryKey: ["products"],
queryFn: fetchProducts,
})
const selected = products?.filter((p) => productIds.includes(p.id))
return (
<button onClick={toggleCart}>
Keranjang ({selected?.length ?? 0}) {cartOpen ? "tutup" : "buka"}
</button>
)
}productIds dari store dan products dari query digabung hanya saat render. selected dihitung dari id yang disimpan di store dan data lengkap dari cache — tidak ada duplikasi, karena store tidak pernah menyimpan objek produk.
Kadang komponen butuh data yang sudah ada di cache tanpa membuat query baru — misalnya setelah mutasi, atau untuk logika non-render. getQueryData adalah pintunya:
import { useQueryClient } from "@tanstack/react-query"
function TodoSummary() {
const queryClient = useQueryClient()
const todos = queryClient.getQueryData(["todos"]) ?? []
const done = todos.filter((t) => t.completed).length
return <p>{done} dari {todos.length} todo selesai</p>
}queryClient.getQueryData(["todos"]) membaca cache tanpa fetch dan tanpa render ulang otomatis. getQueryData mengembalikan data apa adanya — jika ingin berlangganan perubahan, gunakan useQuery biasa.
Aturan emasnya: jangan pernah menyalin data server ke store. Kalau produk yang sama disimpan di cache dan di store, keduanya bisa tidak sinkron, dan invalidasi di TanStack Query tidak akan menyentuh salinan di store. Data server hidup di satu tempat saja: cache TanStack Query.
Setelah mutasi, perbarui cache dengan setQueryData (episode 6 dan 11) sehingga semua komponen yang membaca query tersebut langsung melihat data baru — tanpa perantara store:
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: (newTodo) => {
queryClient.setQueryData(["todos"], (old) => [...(old ?? []), newTodo])
},
})setQueryData(["todos"], ...) menulis langsung ke satu-satunya sumber kebenaran. Semua komponen yang memakai ["todos"] akan ikut ter-update — inilah kekuatan cache sebagai single source of truth.
Warning
Hati-hati menaruh seluruh objek dari query ke dalam store untuk "kemudahan akses". Itu menciptakan salinan kedua yang mudah tidak sinkron. Simpan id, bukan objek, dan biarkan query yang mengambil data.
Meskipun pola hybrid menyarankan TanStack Query untuk server state, ada kasus store tetap diperlukan: state yang benar-benar global dan cepat berubah seperti theme, sidebar state, form multi-langkah, dan keranjang yang perlu dibagikan antar halaman. Intinya bukan menghapus store, tapi membatasi store hanya pada domain UI.
Episode 16 merangkum pola arsitektur modern: TanStack Query untuk server state, Zustand atau Redux untuk UI state, dan cache yang diperlakukan sebagai single source of truth lewat getQueryData dan setQueryData.
Inti yang harus dibawa pulang:
getQueryData membaca cache tanpa fetch atau render ulang.setQueryData setelah mutasi.Di episode 17 selanjutnya kita akan membahas devtools dan debugging — memantau cache dan status query dengan React Query Devtools, serta troubleshooting masalah umum seperti query yang tidak refetch, infinite loop, error serialization, dan memory leak cache.