Manajemen State Skala Besar: Memisahkan Server Cache (TanStack Query) dan Client State (Zustand)

Menghentikan jebakan Redux monolitik: cara memisahkan Server State asinkron menggunakan TanStack Query dan Client State murni menggunakan micro-store Zustand dengan selektor atomik bebas re-render sia-sia.

PuluhLabs Engineering Team, Staff Frontend Engineer · 9 November 2026 · 7 min baca

Manajemen State Skala Besar: Memisahkan Server Cache (TanStack Query) dan Client State (Zustand)
Pengembangan Web
DECOUPLED

Isi artikel ini

  • Memahami Perbedaan Karakteristik Server State vs Client State
  • Mengelola Server Data dengan TanStack Query
  • Mengelola Client State dengan Zustand
  • Panduan Pengambilan Keputusan Arsitektur

Selama bertahun-tahun, komunitas pengembang React terjebak dalam kebiasaan menempatkan seluruh data aplikasi ke dalam satu wadah global terpusat (single global store) menggunakan Redux. Di masa lalu, respons dari REST API, status autentikasi pengguna, status modal pop-up, teks input formulir pencarian, hingga daftar notifikasi dijejalkan ke dalam struktur reducer yang rumit dengan ratusan baris boilerplate action creators dan middleware saga/thunk.

Pendekatan monolitik ini menimbulkan beban perawatan teknis yang berat. Tim pengembang terpaksa menulis logika manual untuk melacak status pemuatan data (isLoading), status galat (error), penanda waktu kadaluarsa data (isStale), serta mekanisme pembatalan request. Lebih buruk lagi, perubahan kecil pada satu bagian state sering kali memicu gelombang re-render sia-sia di seluruh pohon komponen React.

Solusi arsitektur modern yang jauh lebih bersih dan terukur adalah memisahkan penanganan state secara tegas menjadi dua domain yang berbeda: Server Cache (Server State) yang dikelola oleh TanStack Query, dan Client State murni yang dikelola oleh Zustand.

text
Pemisahan Tanggung Jawab State pada Aplikasi Web Modern:
┌───────────────────────────────────────────────────────────────┐
│                    Aplikasi React / Next.js                   │
└──────────────┬────────────────────────────────┬───────────────┘
               │                                │
      (Server Data Asinkron)           (Antarmuka Klien Sinkron)
               ▼                                ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ TanStack Query (React Query) │ │ Zustand Store                │
│ - Data Produk & Stok         │ │ - Status Sidebar Terbuka     │
│ - Riwayat Pesanan            │ │ - Tab Form Multi-Langkah     │
│ - Revalidasi On Window Focus │ │ - Filter Kategori Aktif      │
│ - Deduplikasi HTTP Request   │ │ - Modal Konfirmasi Dialog    │
│ - Optimistic UI Updates      │ │ - Preferensi Tema Gelap      │
└──────────────┬───────────────┘ └──────────────┬───────────────┘
               │                                │
               ▼                                ▼
       [Database / REST API]            [Memori RAM Browser]

Memahami Perbedaan Karakteristik Server State vs Client State #

Kekeliruan terbesar dalam perancangan frontend adalah memperlakukan data yang berasal dari server sama seperti data lokal browser. Keduanya memiliki siklus hidup yang bertolak belakang:

1. Karakteristik Server State #

Data server pada hakikatnya bukan milik browser. Browser hanyalah peminjam snapshot data dari database jarak jauh:

  • Dimiliki dan dikendalikan oleh backend database.
  • Bersifat asinkron dan membutuhkan waktu transmisi jaringan.
  • Dapat diubah oleh pengguna lain kapan saja tanpa sepengetahuan tab browser saat ini (stale data).
  • Membutuhkan strategi caching, penghapusan memori tak terpakai (garbage collection), serta sinkronisasi ulang berkala.

2. Karakteristik Client State #

Client state adalah informasi sementara yang murni mengatur kondisi visual antarmuka:

  • Dimiliki sepenuhnya oleh proses memori tab browser pengguna saat ini.
  • Bersifat sinkron dan dapat diubah seketika tanpa latensi I/O jaringan.
  • Tidak pernah diubah oleh pengguna eksternal lain.
  • Hilang begitu tab browser ditutup atau di-refresh (kecuali dipersistensikan ke localStorage).

Mengelola Server Data dengan TanStack Query #

Dengan TanStack Query, logika pengambilan data, caching, deduplikasi, dan refetching ditangani secara deklaratif. Pengembang tidak perlu lagi menyimpan array data hasil fetch ke dalam Redux atau Zustand.

typescript
// hooks/useProductInventory.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

interface ProductItem {
  id: string;
  name: string;
  stock: number;
  price: number;
}

export function useProductInventory(productId: string) {
  return useQuery({
    queryKey: ['products', productId],
    queryFn: async (): Promise<ProductItem> => {
      const response = await axios.get(`/api/v1/products/${productId}`);
      return response.data;
    },
    // Data dianggap segar selama 2 menit, mencegah request berulang
    staleTime: 1000 * 60 * 2,
    // Data dipertahankan di memori cache selama 10 menit
    gcTime: 1000 * 60 * 10,
    // Refetch otomatis saat pengguna kembali fokus ke tab browser
    refetchOnWindowFocus: true,
  });
}

Untuk operasi mutasi data seperti pembaruan stok dengan umpan balik instan (optimistic updates):

typescript
export function useUpdateStock(productId: string) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (newStock: number) => axios.patch(`/api/v1/products/${productId}`, { stock: newStock }),
    onMutate: async (newStock) => {
      // 1. Batalkan query yang sedang berjalan untuk menghindari race condition
      await queryClient.cancelQueries({ queryKey: ['products', productId] });

      // 2. Simpan snapshot data sebelumnya untuk rollback jika request gagal
      const previousProduct = queryClient.getQueryData<ProductItem>(['products', productId]);

      // 3. Perbarui cache secara optimis di UI tanpa menunggu respons server
      if (previousProduct) {
        queryClient.setQueryData<ProductItem>(['products', productId], {
          ...previousProduct,
          stock: newStock,
        });
      }

      return { previousProduct };
    },
    onError: (err, newStock, context) => {
      // Kembalikan ke state asli jika backend mengembalikan error
      if (context?.previousProduct) {
        queryClient.setQueryData(['products', productId], context.previousProduct);
      }
    },
    onSettled: () => {
      // Sinkronkan kembali dengan data riil database
      queryClient.invalidateQueries({ queryKey: ['products', productId] });
    },
  });
}

Mengelola Client State dengan Zustand #

Untuk state antarmuka yang murni beroperasi di sisi klien (seperti keranjang belanja lokal sebelum checkout atau status panel filter), Zustand memberikan kesederhanaan maksimal dengan footprint ukuran bundle di bawah 2 KB.

Berbeda dengan React Context API yang memaksa render ulang seluruh child components ketika nilai context berubah, Zustand memanfaatkan sistem selective subscription. Komponen hanya akan me-render ulang jika properti spesifik yang dipilihnya mengalami perubahan referensial.

typescript
// store/useCartStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

interface CartItem {
  id: string;
  name: string;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  isDrawerOpen: boolean;
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  toggleDrawer: () => void;
  resetCart: () => void;
}

export const useCartStore = create<CartState>()(
  devtools(
    persist(
      (set) => ({
        items: [],
        isDrawerOpen: false,
        addItem: (newItem) =>
          set((state) => {
            const existingIndex = state.items.findIndex((i) => i.id === newItem.id);
            if (existingIndex > -1) {
              const updated = [...state.items];
              updated[existingIndex].quantity += newItem.quantity;
              return { items: updated };
            }
            return { items: [...state.items, newItem] };
          }),
        removeItem: (id) =>
          set((state) => ({
            items: state.items.filter((i) => i.id !== id),
          })),
        toggleDrawer: () => set((state) => ({ isDrawerOpen: !state.isDrawerOpen })),
        resetCart: () => set({ items: [] }),
      }),
      {
        name: 'puluhlabs_cart_storage',
        // Hanya simpan item keranjang ke localStorage, status drawer tetap di memori
        partialize: (state) => ({ items: state.items }),
      }
    )
  )
);

Penggunaan selektor atomik di dalam komponen mencegah re-render yang tidak diinginkan:

tsx
// components/CartBadge.tsx
import { useCartStore } from '@/store/useCartStore';

export function CartBadge() {
  // Komponen ini HANYA me-render ulang ketika jumlah total item berubah,
  // dan mengabaikan perubahan properti isDrawerOpen!
  const itemCount = useCartStore((state) =>
    state.items.reduce((total, item) => total + item.quantity, 0)
  );

  return (
    <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-800">
      {itemCount}
    </span>
  );
}

Panduan Pengambilan Keputusan Arsitektur #

Ketika merancang fitur baru pada aplikasi frontend skala korporat, gunakan aturan praktis berikut untuk menentukan tempat penyimpanan state:

  1. Gunakan TanStack Query jika: Data berasal dari panggilan HTTP/GraphQL, data harus dibagi antar rute halaman berbeda, data membutuhkan indikator loading/error, atau data perlu di-refresh secara berkala.
  2. Gunakan Zustand jika: State mengatur kontrol UI (modal, panel drawer, dropdown, step wizard), state perlu diakses lintas pohon komponen tanpa prop-drilling, atau data perlu disimpan sementara di browser storage pengguna.
  3. Gunakan React useState lokal jika: Nilai state bersifat terisolasi di dalam satu komponen saja (misalnya teks yang sedang diketik pada satu input formulir sederhana) dan tidak dibutuhkan oleh komponen lain di luar hierarki tersebut.

Dengan disiplin memisahkan kedua domain ini, kode aplikasi frontend menjadi jauh lebih mudah diuji (unit testing), bebas dari belitan boilerplate reducer, dan memiliki performa rendering yang sangat optimal.

Bagikan Artikel Ini:
Topik Terkait:ReactState ManagementTanStack QueryZustandPerforma Web
Istilah Teknis Terkait (Knowledge Graph Glossary)
Jejaring Entitas & Solusi Teknis Terkait
3 Solusi Terintegrasi
Architecture Matrix

Perbandingan Headless CMS vs Monolitik

Kajian komparatif performa, keamanan OWASP, dan total biaya kepemilikan (TCO).

Buka Solusi
Planning Tool

Kalkulator Investasi & Jadwal Website

Estimasi biaya pembuatan website company profile, portal B2B, atau web app kustom.

Buka Solusi
Partnership Hub

Kemitraan White-Label untuk Agensi

Dukungan rekayasa teknis rahasia di balik layar agensi Anda dengan NDA resmi.

Buka Solusi
TO
PuluhLabs Engineering TeamPenulis Terverifikasi

Staff Frontend Engineer

Praktisi dan konsultan pengembangan platform digital terpercaya dengan pengalaman memimpin perancangan lebih dari 50+ proyek untuk skala korporat, bisnis B2B, dan instansi profesional. Berfokus pada pembangunan solusi web yang tangguh, cepat, aman, serta dirancang untuk mendorong pertumbuhan bisnis jangka panjang.

Ditinjau & divalidasi oleh: Tim Profesional PuluhLabs
Terakhir ditinjau: 9 November 2026

Ingin Menerapkan Standar Performa Ini pada Proyek Agensi Anda?

Tim PuluhLabs siap menjadi mitra rekayasa digital rahasia (white-label) untuk agensi Anda. Garansi performa skor Lighthouse 90+, Core Web Vitals hijau, dan 100% hak milik kode.

Konsultasi Proyek Bersama Tim PuluhLabs

Artikel Rekomendasi Selanjutnya

Lihat Semua
  1. Pengembangan Web

    Arsitektur Micro-Frontends vs Modular Monolith: Panduan Memilih untuk Aplikasi Web Skala Enterprise

  2. Pengembangan Web

    Implementasi Zero-Trust Architecture pada REST API dan Webhook Korporat

  3. Pengembangan Web

    Optimasi Query PostgreSQL Skala Jutaan Baris: Indexing, Table Partitioning, dan Connection Pooling