Menata state aplikasi secara profesional: membedakan server state dan client state, memakai TanStack Query untuk data fetching yang robust (caching, retry, invalidation), serta Zustand atau Redux untuk state klien — dipraktikkan di keranjang TokoKita

Saat aplikasi TokoKita tumbuh, muncul masalah klasik: di mana data disimpan? useState per komponen mulai berantakan saat banyak komponen membutuhkan data yang sama. Episode 11 membedah state management & data fetching — topik yang paling sering salah dipahami pemula, karena jawaban yang benar bergantung pada jenis state yang sedang ditangani.
Kesalahan paling umum di 2026: memasukkan data server ke Redux/Zustand, lalu "menemukan ulang" caching yang seharusnya sudah disediakan library khusus. Memahami perbedaan server state vs client state akan menyelamatkan kalian dari ribuan baris boilerplate.
| Server State | Client State | |
|---|---|---|
| Asal | Database/API (di luar aplikasi) | Aplikasi itu sendiri |
| Contoh | Daftar produk, harga, stok | Isi form, tab aktif, tema, item keranjang |
| Sumber kebenaran | Server | Klien |
| Sifat | Async, bisa gagal, bisa basi | Sinkron, langsung di memori |
| Library yang tepat | TanStack Query | Zustand / Redux Toolkit |
Sumber kebenaran server state adalah server — klien hanya mencerminkannya. Karena itu solusinya bukan state manager, melainkan caching layer yang tahu kapan harus fetch, menyimpan, dan menyegarkan.
TanStack Query (sebelumnya React Query) adalah standar de facto data fetching di React. Ia menyediakan caching, retry otomatis, refetch on focus, dan invalidasi — semua hal yang kalian akan "menemukan ulang" secara buruk jika menulis manual.
import { useQuery } from "@tanstack/react-query";
export function useProducts() {
return useQuery({
queryKey: ["products"],
queryFn: async () => {
const res = await fetch("/api/products");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
});
}Penggunaannya deklaratif — komponen tidak peduli bagaimana data di-cache:
export function Catalog() {
const { data, isLoading, isError, error, refetch } = useProducts();
if (isLoading) return <p>Memuat produk…</p>;
if (isError) return <button onClick={() => refetch()}>Coba lagi</button>;
return <ProductGrid products={data} />;
}Tip
queryKey adalah "alamat cache": kunci unik yang menyatukan semua pemakai data yang sama. Jika kalian mengubah data (misal setelah checkout), panggil queryClient.invalidateQueries({ queryKey: ["products"] }) agar daftar produk otomatis diperbarui — tanpa menyentuh komponen yang membaca data itu.
Untuk aksi yang mengubah server (tambah ke keranjang, update profil), pakai useMutation dengan optimistic update — UI merespons instan sebelum server mengonfirmasi:
const addToCart = useMutation({
mutationFn: (productId: number) =>
fetch("/api/cart", { method: "POST", body: JSON.stringify({ productId }) }),
onMutate: (productId) => updateCartCache("+1"), // UI langsung berubah
onError: () => rollbackCartCache(), // gagal → kembalikan
});Pola ini yang membuat UI terasa "seketika" — kunci pengalaman pengguna di episode 12.
Untuk state murni klien, pilih antara Zustand (minimalis, tanpa boilerplate) dan Redux Toolkit (terstruktur, banyak dipakai di perusahaan besar).
import { create } from "zustand";
interface CartState {
items: CartItem[];
add: (item: CartItem) => void;
remove: (id: number) => void;
}
export const useCartStore = create<CartState>((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
remove: (id) => set((s) => ({ items: s.items.filter((i) => i.productId !== id) })),
}));Komponen apa pun membaca dan mengubah store yang sama:
import { useCartStore } from "@/store/cart";
export function CartButton() {
const items = useCartStore((s) => s.items);
return <button>Tambah ({items.length})</button>;
}Zustand dipilih saat ingin cepat tanpa konsep tambahan; Redux Toolkit saat tim besar butuh struktur ketat (slices, devtools, middleware). Keduanya sah — pilih sesuai kebutuhan tim.
Arsitektur state TokoKita yang disarankan:
useQuery(["products"]), cache di TanStack Query.useMutation + optimistic update.["cart"] dan ["products"] (stok berkurang).Server state: products, stock, cart (di DB)
→ TanStack Query: ["products"], ["cart"]
Client state: tampilan, form, filter, tema
→ Zustand: useCartStore (hanya transisi)| Kesalahan | Dampak | Perbaikan |
|---|---|---|
| Menaruh server state di Redux/Zustand | Boilerplate + cache basi | TanStack Query |
| Fetch manual di tiap komponen | Duplikasi & no caching | Satu queryFn per resource |
Query tanpa queryKey stabil | Cache tidak terpakai | Key unik per resource |
| Mutasi tanpa error handling | Data divergen | onError + rollback |
| Store global untuk semuanya | Komponen re-render tak perlu | State di dekat pemakainya |
Inti yang harus dibawa pulang:
queryFn satu-satunya sumber data.Di episode 12 selanjutnya kita mengukur hasil kerja kita: web performance dengan Core Web Vitals — LCP, INP, CLS, code-splitting, lazy loading, dan profiling, agar TokoKita terasa instan di perangkat pengguna nyata. Sampai jumpa di episode 12!