React Zustand

Target: Pemula yang telah memahami React Dasar dan React Router, serta ingin menguasai Global State Management modern, ringan, berkinerja tinggi, dan tanpa Provider boilerplate menggunakan Zustand v4 / v5 (React 18 / 19 & Vite). Versi: Zustand 4.x / 5.x Prasyarat: React Dasar Fokus modul pembelajaran ini: masalah Props Drilling → kelemahan React Context API → mental model Zustand → fungsi create() → State & Actions → Selector-based Subscriptions (performa render optimal) → Immutability Array & Object → parameter get → Async Actions & API fetch → persist LocalStorage middleware → Redux devtools middleware → Store Slices Pattern → useStore.getState() di luar React → useShallow → Client State vs Server State → mini project E-Commerce Shopping Cart & Auth Manager.


Cara Belajar

🟢 Fundamental
→ wajib dipahami: Masalah Props Drilling, Context vs Zustand, fungsi create(), State & Actions, dan State Selectors
 
🟡 Lanjutan
→ pelajari setelah menguasai Store dasar: Parameter get, Async Actions, Persist Middleware (LocalStorage), DevTools, dan Store Slices
 
🔴 Advanced / Operasional
→ penting untuk arsitektur production: getState() di luar React (Axios), useShallow optimization, dan Client State vs Server State

Mental model alur kerja Zustand Global State:

               1. KOMPONEN MEMANGGIL ACTION (useStore)
                   addItem({ id: "P1", name: "Laptop" })


               2. ZUSTAND STORE (create / set / get)
                   Perbarui State Cart & Kalkulasi Total


               3. PERSIST MIDDLEWARE (Opsional)
                   Otomatis Simpan ke LocalStorage


               4. SELECTOR BERLANGGANAN (Subscribers)
      ┌─────────────────────────┴─────────────────────────┐
      ▼ (Komponen yang Memilih cartItems)                   ▼ (Komponen yang Memilih user)
  CartDrawer RE-RENDER (UI Update)                  UserAvatar TIDAK RE-RENDER (Aman!)

Hafalan:

Props Drilling       → kondisi buruk di mana data dioper melalui banyak layer komponen perantara yang sebenarnya tidak butuh data tersebut
Zustand              → library global state management berbasis Hook yang sangat ringkas, cepat, dan tidak memerlukan <Provider> pembungkus
create((set, get))   → fungsi inisialisasi untuk membuat Store kustom yang menampung variabel state dan action functions
set(fnOrObject)      → fungsi mutator di dalam store untuk memperbarui state (otomatis melakukan shallow merge tingkat atas)
get()                → fungsi di dalam store untuk membaca state terkini tanpa perlu memicu set pembaruan
Selector             → fungsi pemilih `state => state.target` yang memastikan komponen hanya me-render ulang jika nilai target berubah
persist Middleware   → plugin resmi Zustand untuk menyinkronkan state ke web storage (localStorage / sessionStorage) secara otomatis
getState()           → metode imperatif untuk membaca data store langsung dari file JavaScript biasa di luar komponen React

Daftar Isi

🟢 Fundamental

  1. Pengenalan Global State Management & Masalah Props Drilling di React
  2. Evaluasi React Context API (useContext) vs Zustand
  3. Pengenalan Zustand & Mental Model
  4. Instalasi & Setup Dasar Zustand
  5. Membuat Store Pertama dengan create()
  6. Mengonsumsi Store di Komponen & Konsep State Selectors
  7. Pembaruan State Objek & Array Immutability di Zustand
  8. Mengakses State Terkini di Dalam Action dengan Parameter get

🟡 Lanjutan

  1. Async Actions di Zustand
  2. Auto-Sync LocalStorage dengan persist Middleware
  3. Debugging State dengan Redux DevTools Middleware (devtools)
  4. Menggabungkan Multiple Middleware
  5. Immer Middleware untuk Mutasi State yang Lebih Mudah
  6. Memisahkan Store Besar dengan Slices Pattern
  7. Membaca & Mengubah State di Luar Komponen React
  8. Menghindari Re-render Berlebihan dengan useShallow

🔴 Advanced / Operasional

  1. Perbandingan Arsitektur: Client State (Zustand) vs Server State (TanStack React Query)
  2. TypeScript Support Dasar di Zustand

🛠️ Referensi & Praktik

  1. Peta Ingatan Cepat
  2. Tabel Ringkasan
  3. Cheat Code Zustand 10 Detik
  4. Urutan Belajar yang Disarankan
  5. Mini Project: Production-Ready E-Commerce Shopping Cart & Auth State Manager Web App
  6. Referensi Resmi

1. 🟢 Pengenalan Global State Management & Masalah Props Drilling di React

Konsep

Ketika membangun aplikasi kecil, state lokal (useState) dan Lifting State Up sudah memadai. Namun saat aplikasi membesar, data tertentu (seperti data User Login, Keranjang Belanja, Notifikasi, atau Tema) dibutuhkan oleh puluhan komponen yang tersebar di pohon hierarki yang berbeda.

Props Drilling: Kondisi di mana kita terpaksa mengoper props melalui 5–10 lapisan komponen perantara yang sama sekali tidak membutuhkan data tersebut, hanya agar data bisa sampai ke komponen cucu di dasar pohon. Hal ini membuat kode sangat rapuh, kotor, dan sulit di-refactor.

Global State Management: Menyimpan state bersama di dalam Store Terpusat (Single Source of Truth) di luar pohon komponen. Komponen manapun dapat langsung membaca atau mengubah data tersebut tanpa melewati perantara.

Cara Kerja

Props Drilling (Buruk):
App ──(props)──> Layout ──(props)──> Header ──(props)──> Navbar ──(props)──> UserAvatar
 
Global State Store (Zustand - Bersih):
┌────────────────────────────┐
│    Global Zustand Store    │
└──────┬──────────────┬──────┘
       │ (Direct)     │ (Direct)
       ▼              ▼
   Navbar         UserAvatar

Hafalan:

Global State Store → wadah data terpusat independen yang dapat diakses langsung oleh komponen manapun tanpa perantara

2. 🟢 Evaluasi React Context API (useContext) vs Zustand

Konsep

Banyak pemula bertanya: “React sudah punya Context API bawaan, mengapa kita butuh library seperti Zustand?”

Perbandingan:

Fitur / KarakteristikReact Context APIZustand
Kebutuhan ProviderWajib membungkus JSX dengan <MyContext.Provider> (Menyebabkan Provider Hell)Nol Provider. Cukup import custom hook dan pakai langsung
Performa Re-renderBuruk untuk state dinamis. Setiap ada 1 nilai di context yang berubah, seluruh komponen konsumen akan me-render ulangSuper Cepat. Mendukung Selectors, komponen hanya re-render jika field yang dipilihnya berubah
BoilerplatePanjang (createContext, useContext, Provider wrapper)Sangat singkat (Hanya 1 fungsi create())
Akses di luar React❌ Tidak bisa diakses di file .js biasa✅ Bisa diakses kapan saja via useStore.getState()

Kesimpulan: Gunakan Context API hanya untuk data yang sangat jarang berubah (seperti Bahasa/Locale atau Tema). Gunakan Zustand untuk seluruh state dinamis aplikasi (Auth, Keranjang, Data Form, Filter).

Hafalan:

Zustand vs Context → Zustand unggul mutlak dalam performa re-render (selector-based) dan bebas dari pembungkus Provider

3. 🟢 Pengenalan Zustand & Mental Model

Konsep

Zustand (kata Jerman yang berarti “State / Keadaan”) adalah library state management berukuran sangat kecil (~1 kB) yang dirancang dengan filosofi modern:

  1. Hook-Centric: Store yang Anda buat otomatis menjadi React Hook (misal: useCartStore()).
  2. Unopinionated & Minimalist: Tidak ada konsep action types string yang rumit, reducer switch-case raksasa, atau dispatcher kaku seperti Redux lama.
  3. Immutability Otomatis Tingkat Atas: Fungsi set() secara otomatis melakukan shallow merge untuk properti tingkat atas.

Hafalan:

Zustand Store → custom hook mandiri yang menggabungkan deklarasi state dan fungsi mutator dalam satu kesatuan

4. 🟢 Instalasi & Setup Dasar Zustand

Konsep

Instalasi Zustand pada proyek React / Vite:

npm install zustand

Tidak ada konfigurasi tambahan atau setup file global yang diperlukan. Anda bisa langsung membuat file store (misal: src/store/useCounterStore.js).

Hafalan:

npm install zustand → dependensi tunggal ringan tanpa dependensi peer yang memberatkan

5. 🟢 Membuat Store Pertama dengan create()

Konsep

Fungsi create() dari zustand menerima satu parameter callback (set) => ({ ... }) yang mengembalikan objek penampung State dan Action Functions.

Fungsi set():

  • Digunakan untuk memperbarui nilai state di dalam store.
  • Dapat menerima objek baru: set({ count: 10 }).
  • Atau menerima fungsi updater: set((state) => ({ count: state.count + 1 })).

Contoh

// src/store/useCounterStore.js
import { create } from 'zustand';
 
export const useCounterStore = create((set) => ({
  // 1. Variabel State
  count: 0,
  userName: "Budi Santoso",
 
  // 2. Action Functions
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  setCustomCount: (newVal) => set({ count: newVal }),
  reset: () => set({ count: 0 })
}));

Hafalan:

export const useStore = create((set) => ({ key: value, action: () => set(fn) }))

6. 🟢 Mengonsumsi Store di Komponen & Konsep State Selectors

Konsep

Untuk menggunakan store di dalam komponen React, kita memanggil custom hook hasil create() dengan memberikan fungsi Selector: state => state.targetProperty.

Mengapa Wajib Menggunakan Selector?

  • Jika Anda menulis: const store = useCounterStore() Komponen akan me-render ulang setiap kali properti apapun di dalam store berubah (Boros performa!).
  • Jika Anda menulis: const count = useCounterStore(state => state.count) Komponen HANYA akan me-render ulang jika nilai count yang berubah. Jika userName berubah, komponen ini tidak akan terganggu.

Contoh

import React from 'react';
import { useCounterStore } from '../store/useCounterStore';
 
export default function CounterDisplay() {
  // Selector spesifik (Hanya berlangganan ke 'count')
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);
  const decrement = useCounterStore((state) => state.decrement);
 
  return (
    <div style={{ textAlign: "center", padding: "20px" }}>
      <h2>Hitungan: {count}</h2>
      <button onClick={decrement}>- Kurang</button>
      <button onClick={increment} style={{ marginLeft: "8px" }}>+ Tambah</button>
    </div>
  );
}

Hafalan:

const value = useStore((state) => state.specificProperty) → selector berlangganan presisi untuk performa render maksimal

7. 🟢 Pembaruan State Objek & Array Immutability di Zustand

Konsep

Fungsi set() di Zustand melakukan shallow merge otomatis pada level pertama, tetapi untuk Array dan Objek bersarang, Anda tetap wajib menerapkan prinsip Immutability:

  • Menambah ke Array: set(state => ({ items: [...state.items, newItem] }))
  • Menghapus dari Array: set(state => ({ items: state.items.filter(i => i.id !== targetId) }))
  • Memperbarui Objek Bersarang: set(state => ({ user: { ...state.user, name: newName } }))

Contoh

import { create } from 'zustand';
 
export const useTodoStore = create((set) => ({
  todos: [
    { id: 1, text: "Belajar Zustand", completed: false }
  ],
 
  // Tambah Todo Baru
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, { id: Date.now(), text, completed: false }]
  })),
 
  // Toggle Status Selesai
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map((todo) =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )
  })),
 
  // Hapus Todo
  deleteTodo: (id) => set((state) => ({
    todos: state.todos.filter((todo) => todo.id !== id)
  }))
}));

Hafalan:

set(state => ({ items: [...state.items, newItem] }))       → menambah item array di Zustand store
set(state => ({ items: state.items.filter(i => i.id !== id) })) → menghapus item array di Zustand store

8. 🟢 Mengakses State Terkini di Dalam Action dengan Parameter get

Konsep

Seringkali di dalam action function, kita perlu membaca data state saat ini tanpa harus memodifikasinya (misal: memvalidasi stok barang sebelum dimasukkan ke keranjang, atau menghitung total harga).

Callback create() menyediakan parameter kedua yaitu get: create((set, get) => ({ ... }))

  • get().items : Membaca nilai array items saat ini.
  • get().totalPrice() : Memanggil helper kalkulasi.

Contoh

import { create } from 'zustand';
 
export const useCartStore = create((set, get) => ({
  items: [],
 
  // Action dengan validasi membaca state via get()
  addItemWithCheck: (product) => {
    const currentItems = get().items;
    const isAlreadyInCart = currentItems.some((i) => i.id === product.id);
 
    if (isAlreadyInCart) {
      alert("Produk ini sudah ada di keranjang!");
      return;
    }
 
    set({ items: [...currentItems, { ...product, quantity: 1 }] });
  },
 
  // Helper kalkulasi total belanja langsung dari store
  getTotalAmount: () => {
    return get().items.reduce((total, item) => total + item.price * item.quantity, 0);
  }
}));

Hafalan:

const currentState = get(); → membaca snapshot data state di dalam action function tanpa memicu mutasi

9. 🟡 Async Actions di Zustand

Konsep

Di Redux lama, menangani asynchronous data fetching membutuhkan middleware rumit seperti Redux Thunk atau Redux Saga.

Di Zustand, Async Action bekerja secara alami: Cukup tambahkan kata kunci async pada action function, dan panggil set() saat status loading berubah dan saat data API tiba!

Contoh

import { create } from 'zustand';
 
export const useProductStore = create((set) => ({
  products: [],
  isLoading: false,
  errorMessage: null,
 
  // Async Action Natural
  fetchProducts: async () => {
    set({ isLoading: true, errorMessage: null });
    try {
      const response = await fetch("https://fakestoreapi.com/products?limit=5");
      if (!response.ok) throw new Error("Gagal mengambil data produk dari server");
      const data = await response.json();
      set({ products: data, isLoading: false });
    } catch (error) {
      set({ errorMessage: error.message, isLoading: false });
    }
  }
}));

Penggunaan di Komponen:

import { useEffect } from 'react';
import { useProductStore } from '../store/useProductStore';
 
export default function ProductList() {
  const { products, isLoading, errorMessage, fetchProducts } = useProductStore();
 
  useEffect(() => {
    fetchProducts();
  }, [fetchProducts]);
 
  if (isLoading) return <p>⏳ Sedang memuat produk...</p>;
  if (errorMessage) return <p style={{ color: "red" }}>❌ {errorMessage}</p>;
 
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.title} - ${p.price}</li>)}
    </ul>
  );
}

Hafalan:

actionName: async () => { set({ loading: true }); const res = await fetch(); set({ data: res, loading: false }); }

10. 🟡 Auto-Sync LocalStorage dengan persist Middleware

Konsep

Zustand menyediakan middleware resmi persist untuk menyimpan state ke localStorage (atau sessionStorage) secara otomatis:

  • Setiap ada perubahan state Zustand otomatis men-serialize state ke JSON di localStorage.
  • Saat browser di-refresh Zustand otomatis memuat kembali (Hydrate) data dari localStorage ke state aplikasi.

Contoh

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
 
export const useAuthStore = create(
  persist(
    (set) => ({
      user: null,
      token: null,
 
      login: (userData, token) => set({ user: userData, token }),
      logout: () => set({ user: null, token: null })
    }),
    {
      name: "auth-storage", // Kunci di LocalStorage
      storage: createJSONStorage(() => localStorage), // Default: localStorage
      // Opsional: hanya simpan field tertentu (partialize)
      partialize: (state) => ({ token: state.token, user: state.user })
    }
  )
);

Hafalan:

create(persist((set, get) => ({ ... }), { name: "storage-key" })) → auto-sync state ke LocalStorage otomatis

11. 🟡 Debugging State dengan Redux DevTools Middleware (devtools)

Konsep

Anda dapat menggunakan ekstensi browser Redux DevTools untuk melakukan Time-Travel Debugging, melihat riwayat setiap mutasi state, dan membatalkan aksi (Undo/Redo), meskipun Anda menggunakan Zustand!

Cukup bungkus store Anda dengan middleware devtools.

Contoh

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
 
export const useCounterStore = create(
  devtools(
    (set) => ({
      count: 0,
      // Berikan label nama action sebagai parameter kedua di set() untuk log DevTools
      increment: () => set((state) => ({ count: state.count + 1 }), false, "counter/increment"),
      decrement: () => set((state) => ({ count: state.count - 1 }), false, "counter/decrement")
    }),
    { name: "CounterStore" }
  )
);

Hafalan:

create(devtools((set) => ({ ... }), { name: "StoreName" })) → mengaktifkan debugging Redux DevTools di browser

12. 🟡 Menggabungkan Multiple Middleware

Konsep

Dalam aplikasi profesional, kita sering menggabungkan beberapa middleware sekaligus (misal: devtools untuk debugging + persist untuk penyimpanan lokal).

Pola pembungkusannya: create(devtools(persist((set, get) => ({ ... }), { name: "storage-key" }), { name: "DevToolsName" }))

Contoh

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
 
export const useSettingsStore = create(
  devtools(
    persist(
      (set) => ({
        theme: "light",
        language: "id",
        toggleTheme: () => set((s) => ({ theme: s.theme === "light" ? "dark" : "light" }))
      }),
      { name: "app-settings-storage" }
    ),
    { name: "SettingsStore" }
  )
);

Hafalan:

create(devtools(persist((set) => ({ ... }), { name: "storage-key" }))) → menggabungkan DevTools & Persist

13. 🟡 Immer Middleware untuk Mutasi State yang Lebih Mudah

Konsep

Jika Anda memiliki data state dengan struktur bersarang yang sangat dalam (deeply nested objects/arrays), menulis spread operator ... berulang kali dapat membingungkan dan rawan salah.

Middleware immer memungkinkan kita menulis mutasi langsung (Direct Mutation) seperti JavaScript biasa (misal: state.cart.items.push(item) atau state.user.profile.age++), dan Immer akan otomatis mengonversinya menjadi immutable state baru di balik layar.

npm install immer

Contoh

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
 
export const useNestedStore = create(
  immer((set) => ({
    user: {
      profile: { name: "Budi", details: { score: 100 } }
    },
    updateScore: (newScore) =>
      set((state) => {
        // Mutasi langsung tanpa spread operator berjenjang!
        state.user.profile.details.score = newScore;
      })
  }))
);

Hafalan:

create(immer((set) => ({ update: () => set(state => { state.nested.val = newVal }) })))

14. 🟡 Memisahkan Store Besar dengan Slices Pattern

Konsep

Untuk aplikasi enterprise yang memiliki ratusan state, menyatukan seluruh state ke dalam satu file raksasa akan sulit dikelola.

Slices Pattern:

  1. Buat beberapa slice modular (misal: createAuthSlice, createCartSlice).
  2. Gabungkan seluruh slice ke dalam satu store utama menggunakan operator spread.

Contoh

Slice 1 (src/store/slices/authSlice.js):

export const createAuthSlice = (set) => ({
  user: null,
  setUser: (user) => set({ user }),
  logout: () => set({ user: null })
});

Slice 2 (src/store/slices/cartSlice.js):

export const createCartSlice = (set) => ({
  items: [],
  addItem: (product) => set((s) => ({ items: [...s.items, product] })),
  clearCart: () => set({ items: [] })
});

Store Utama (src/store/useAppStore.js):

import { create } from 'zustand';
import { createAuthSlice } from './slices/authSlice';
import { createCartSlice } from './slices/cartSlice';
 
export const useAppStore = create((...a) => ({
  ...createAuthSlice(...a),
  ...createCartSlice(...a)
}));

Hafalan:

Slices Pattern → memecah store raksasa menjadi potongan modul slice terpisah yang digabung di root store

15. 🟡 Membaca & Mengubah State di Luar Komponen React

Konsep

Salah satu keunggulan terbesar Zustand dibanding React Context adalah: Zustand dapat dibaca dan diubah dari file JavaScript murni di luar siklus render React (misal: di dalam Axios Interceptor, file utilitas, atau service worker).

  • Membaca State: useStore.getState().token
  • Mengubah State: useStore.setState({ token: "newToken" })
  • Berlangganan Perubahan Manual: useStore.subscribe((state) => console.log(state))

Contoh (Axios Authorization Interceptor)

// src/api/axiosClient.js
import axios from 'axios';
import { useAuthStore } from '../store/useAuthStore';
 
const apiClient = axios.create({ baseURL: 'https://api.toko.com/v1' });
 
// Request Interceptor: Otomatis sisipkan Bearer Token dari Zustand
apiClient.interceptors.request.use((config) => {
  // Akses state langsung di luar komponen React!
  const token = useAuthStore.getState().token;
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});
 
export default apiClient;

Hafalan:

useStore.getState().property → membaca data store secara instan di file JavaScript biasa tanpa React Hook
useStore.setState({ key: val }) → mengubah state secara imperatif dari luar komponen

16. 🟡 Menghindari Re-render Berlebihan dengan useShallow

Konsep

Jika Anda ingin memilih beberapa nilai sekaligus dari store dalam bentuk objek: const { name, role } = useUserStore(state => ({ name: state.name, role: state.role }))

Secara default, React menganggap objek { name, role } baru sebagai referensi berbeda pada setiap render (Reference Inequality), yang memicu re-render tak terbatas / berulang!

Solusi: Gunakan utility useShallow dari zustand/react/shallow: Zustand akan membandingkan nilai properti satu per satu (Shallow Comparison) dan mencegah re-render jika nilainya sama persis.

Contoh

import { useShallow } from 'zustand/react/shallow';
import { useUserStore } from '../store/useUserStore';
 
export default function UserBadge() {
  // Aman dari re-render berlebihan saat mengekstrak multiple properties!
  const { name, role } = useUserStore(
    useShallow((state) => ({
      name: state.name,
      role: state.role
    }))
  );
 
  return <div>{name} ({role})</div>;
}

Hafalan:

useStore(useShallow(state => ({ a: state.a, b: state.b }))) → mengekstrak banyak field tanpa bug re-render objek baru

17. 🔴 Perbandingan Arsitektur: Client State (Zustand) vs Server State (TanStack React Query)

Konsep

Standar arsitektur frontend modern membagi state menjadi 2 domain yang berbeda:

  1. Client State (Kelola dengan Zustand):
    • Data lokal yang dimiliki dan dikendalikan penuh oleh aplikasi frontend browser.
    • Contoh: Keranjang belanja lokal, Status Toggle Modal/Sidebar, Pilihan Tema, Input form sementara.
  2. Server State (Kelola dengan TanStack Query / React Query):
    • Data yang dimiliki oleh database backend/server.
    • Membutuhkan penanganan caching, revalidation, deduplication, dan polling.
    • Contoh: Daftar produk dari database, riwayat invoice order.

Hafalan:

Client State (Zustand) = data UI milik browser | Server State (TanStack Query) = data caching milik database server

18. 🔴 TypeScript Support Dasar di Zustand

Konsep

Zustand memiliki dukungan TypeScript kelas satu bawaan tanpa perlu package types tambahan:

Definisikan tipe interface untuk State dan Actions, lalu operasikan ke create<StoreType>():

Contoh

import { create } from 'zustand';
 
interface CartState {
  totalItems: number;
  isOpen: boolean;
  addItem: () => void;
  toggleDrawer: () => void;
}
 
export const useCartStore = create<CartState>()((set) => ({
  totalItems: 0,
  isOpen: false,
  addItem: () => set((state) => ({ totalItems: state.totalItems + 1 })),
  toggleDrawer: () => set((state) => ({ isOpen: !state.isOpen })),
}));

Hafalan:

create<StoreInterface>()((set) => ({ ... })) → pola deklarasi typed store di TypeScript

19. 🛠️ Peta Ingatan Cepat

                          PETA ARSITEKTUR ZUSTAND

       ┌─────────────────────────────┼─────────────────────────────┐
       ▼                             ▼                             ▼
CORE & SELECTORS             ASYNC & MIDDLEWARE            INTEGRASI & ADVANCED
├─ create((set, get))        ├─ async/await Actions        ├─ useStore.getState() (Axios)
├─ state => state.target     ├─ persist (LocalStorage)     ├─ useShallow (Multi-field)
├─ Immutability Spread       ├─ devtools (Redux Tool)      ├─ Slices Pattern
└─ Zero Provider Setup       └─ immer (Direct Mutate)      └─ Client vs Server State

20. 📚 Tabel Ringkasan

Komponen / FiturLokasiFungsi & Karakteristik Utama
create((set, get))zustandFungsi inti pembuat Custom Hook Store global
set(fnOrObject)Store ActionMemperbarui state dengan shallow merge otomatis
get()Store ActionMembaca snapshot data state saat ini di dalam action
SelectorKomponenFungsi pemfilter state => state.val penangkal re-render
persistzustand/middlewareMenyimpan dan me-load state ke LocalStorage otomatis
devtoolszustand/middlewareMenghubungkan mutasi state ke Redux DevTools extension
immerzustand/middleware/immerMemungkinkan sintaks mutasi langsung pada nested state
useShallowzustand/react/shallowMencegah re-render saat memilih multiple properti
getState()Instance StoreMembaca state di file JavaScript biasa di luar React

21. ⚡ Cheat Code Zustand 10 Detik

// 1. Template Store Persist Lengkap
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
 
export const useStore = create(
  persist(
    (set, get) => ({
      items: [],
      add: (item) => set(s => ({ items: [...s.items, item] })),
      remove: (id) => set(s => ({ items: s.items.filter(i => i.id !== id) })),
      count: () => get().items.length,
    }),
    { name: 'app-storage' }
  )
);
 
// 2. Pemakaian di Komponen
const items = useStore(state => state.items);
const add = useStore(state => state.add);

22. 🧭 Urutan Belajar yang Disarankan

Langkah 1: Fundamental Store & Selectors
├── Kuasai create((set)) dan buat State serta Actions sederhana
└── Gunakan State Selectors (state => state.val) di komponen


Langkah 2: Parameter get & Async Actions
├── Akses data state via get() untuk kalkulasi dan validasi
└── Buat Async Actions untuk fetch data API langsung di store


Langkah 3: Middleware Produktivitas
├── Terapkan persist middleware untuk auto-sync ke LocalStorage
└── Pasang devtools middleware untuk debugging


Langkah 4: Pola Arsitektur Skala Besar
├── Pecah store menjadi modul modular dengan Slices Pattern
├── Akses store di luar React via useStore.getState() (API Interceptors)
└── Optimasi performa re-render objek via useShallow


Langkah 5: Siap Membangun Aplikasi Enterprise Skala Penuh!

23. 🏗️ Mini Project: Production-Ready E-Commerce Shopping Cart & Auth State Manager Web App

Aplikasi web lengkap dan runnable yang mengintegrasikan: Zustand Store dengan persist Middleware (LocalStorage), Auth State, Shopping Cart State, Live Calculation Subscriptions, Selectors, Multi-Component Dispatching, dan Toast Notifications.

import React, { useState } from 'react';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
 
// ==========================================
// 1. GLOBAL ZUSTAND STORE WITH PERSIST
// ==========================================
export const useECommerceStore = create(
  persist(
    (set, get) => ({
      // --- AUTH STATE ---
      user: null,
      login: (username) => set({ user: { username, role: "CUSTOMER" } }),
      logout: () => set({ user: null, cart: [] }),
 
      // --- CART STATE ---
      cart: [],
 
      // Tambah Barang ke Keranjang
      addToCart: (product) => {
        const currentCart = get().cart;
        const existingItem = currentCart.find(item => item.id === product.id);
 
        if (existingItem) {
          // Jika sudah ada, tambah kuantitas
          set({
            cart: currentCart.map(item =>
              item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
            )
          });
        } else {
          // Jika belum ada, masukkan item baru
          set({ cart: [...currentCart, { ...product, quantity: 1 }] });
        }
      },
 
      // Update Kuantitas
      updateQuantity: (id, amount) => {
        const currentCart = get().cart;
        set({
          cart: currentCart
            .map(item => {
              if (item.id === id) {
                const newQty = item.quantity + amount;
                return newQty > 0 ? { ...item, quantity: newQty } : null;
              }
              return item;
            })
            .filter(Boolean) // Hapus item jika quantity = 0
        });
      },
 
      // Hapus Item
      removeFromCart: (id) => {
        set(state => ({ cart: state.cart.filter(item => item.id !== id) }));
      },
 
      // Kosongkan Keranjang
      clearCart: () => set({ cart: [] }),
 
      // --- CALCULATED GETTERS ---
      getTotalPrice: () => {
        return get().cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
      },
 
      getTotalItemCount: () => {
        return get().cart.reduce((sum, item) => sum + item.quantity, 0);
      }
    }),
    {
      name: "ecommerce-zustand-storage" // Otomatis tersimpan di LocalStorage
    }
  )
);
 
// ==========================================
// 2. DUMMY PRODUCTS CATALOG
// ==========================================
const PRODUCTS = [
  { id: "P1", name: "Mechanical Keyboard RGB", price: 850000, category: "AKSESORIS" },
  { id: "P2", name: "Wireless Ergonomic Mouse", price: 450000, category: "AKSESORIS" },
  { id: "P3", name: "Headset Gaming 7.1", price: 650000, category: "AUDIO" }
];
 
// ==========================================
// 3. KOMPONEN NAVBAR (SELECTOR KE USER & TOTAL ITEMS)
// ==========================================
function Navbar() {
  // Hanya berlangganan ke field user dan cart
  const user = useECommerceStore(state => state.user);
  const login = useECommerceStore(state => state.login);
  const logout = useECommerceStore(state => state.logout);
  const totalItemCount = useECommerceStore(state => state.getTotalItemCount());
 
  return (
    <header style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "16px 24px", background: "#1e293b", color: "#fff", borderRadius: "8px", marginBottom: "24px" }}>
      <h2 style={{ margin: 0, fontSize: "20px" }}>⚡ Zustand Mart</h2>
 
      <div style={{ display: "flex", alignItems: "center", gap: "20px" }}>
        <div style={{ background: "#334155", padding: "6px 12px", borderRadius: "20px", fontWeight: "bold" }}>
          🛒 Keranjang: <span style={{ color: "#38bdf8" }}>{totalItemCount} Barang</span>
        </div>
 
        {user ? (
          <div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
            <span>Halo, <strong>{user.username}</strong></span>
            <button onClick={logout} style={{ background: "#ef4444", color: "#fff", border: "none", padding: "6px 12px", borderRadius: "4px", cursor: "pointer" }}>
              Logout
            </button>
          </div>
        ) : (
          <button onClick={() => login("Budi Santoso")} style={{ background: "#10b981", color: "#fff", border: "none", padding: "6px 14px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold" }}>
            Login Demo
          </button>
        )}
      </div>
    </header>
  );
}
 
// ==========================================
// 4. KOMPONEN KATALOG PRODUK
// ==========================================
function ProductCatalog() {
  const addToCart = useECommerceStore(state => state.addToCart);
 
  return (
    <div style={{ marginBottom: "30px" }}>
      <h3>Katalog Produk Tersedia</h3>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "16px" }}>
        {PRODUCTS.map(p => (
          <div key={p.id} style={{ border: "1px solid #e2e8f0", padding: "16px", borderRadius: "8px", background: "#f8fafc" }}>
            <h4 style={{ margin: "0 0 8px 0" }}>{p.name}</h4>
            <div style={{ color: "#64748b", fontSize: "13px", marginBottom: "8px" }}>{p.category}</div>
            <div style={{ fontWeight: "bold", color: "#16a34a", fontSize: "16px", marginBottom: "14px" }}>
              Rp {p.price.toLocaleString("id-ID")}
            </div>
            <button
              onClick={() => addToCart(p)}
              style={{ width: "100%", background: "#2563eb", color: "#fff", border: "none", padding: "8px", borderRadius: "6px", cursor: "pointer", fontWeight: "bold" }}
            >
              + Masukkan Keranjang
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}
 
// ==========================================
// 5. KOMPONEN CART DRAWER (BELANJA & CHECKOUT)
// ==========================================
function CartSummary() {
  const cart = useECommerceStore(state => state.cart);
  const updateQuantity = useECommerceStore(state => state.updateQuantity);
  const removeFromCart = useECommerceStore(state => state.removeFromCart);
  const clearCart = useECommerceStore(state => state.clearCart);
  const totalPrice = useECommerceStore(state => state.getTotalPrice());
 
  if (cart.length === 0) {
    return (
      <div style={{ border: "2px dashed #cbd5e1", padding: "30px", textAlign: "center", borderRadius: "8px", color: "#64748b" }}>
        Keranjang belanja Anda masih kosong. Silakan pilih produk di atas!
      </div>
    );
  }
 
  return (
    <div style={{ border: "1px solid #e2e8f0", padding: "20px", borderRadius: "8px", background: "#ffffff" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "16px" }}>
        <h3 style={{ margin: 0 }}>Rincian Keranjang Belanja</h3>
        <button onClick={clearCart} style={{ background: "#fee2e2", color: "#dc2626", border: "none", padding: "4px 8px", borderRadius: "4px", cursor: "pointer" }}>
          Kosongkan
        </button>
      </div>
 
      <ul style={{ listStyle: "none", padding: 0, margin: "0 0 20px 0" }}>
        {cart.map(item => (
          <li key={item.id} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "12px 0", borderBottom: "1px solid #f1f5f9" }}>
            <div>
              <div style={{ fontWeight: "bold" }}>{item.name}</div>
              <div style={{ color: "#64748b", fontSize: "13px" }}>
                Rp {item.price.toLocaleString("id-ID")} x {item.quantity} = <strong>Rp {(item.price * item.quantity).toLocaleString("id-ID")}</strong>
              </div>
            </div>
 
            <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
              <button onClick={() => updateQuantity(item.id, -1)} style={{ padding: "4px 8px", cursor: "pointer" }}>-</button>
              <span style={{ fontWeight: "bold", minWidth: "20px", textAlign: "center" }}>{item.quantity}</span>
              <button onClick={() => updateQuantity(item.id, 1)} style={{ padding: "4px 8px", cursor: "pointer" }}>+</button>
              <button onClick={() => removeFromCart(item.id)} style={{ color: "#ef4444", border: "none", background: "none", cursor: "pointer", marginLeft: "10px" }}>✕</button>
            </div>
          </li>
        ))}
      </ul>
 
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", borderTop: "2px solid #e2e8f0", paddingTop: "16px" }}>
        <div>
          <small style={{ color: "#64748b" }}>Total Pembayaran:</small>
          <div style={{ fontSize: "22px", fontWeight: "bold", color: "#2563eb" }}>
            Rp {totalPrice.toLocaleString("id-ID")}
          </div>
        </div>
        <button
          onClick={() => alert(`Pembayaran sebesar Rp ${totalPrice.toLocaleString("id-ID")} Berhasil!`)}
          style={{ background: "#16a34a", color: "#fff", border: "none", padding: "10px 24px", borderRadius: "6px", cursor: "pointer", fontWeight: "bold", fontSize: "15px" }}
        >
          Checkout Sekarang ➡
        </button>
      </div>
    </div>
  );
}
 
// ==========================================
// 6. MAIN APPLICATION
// ==========================================
export default function App() {
  return (
    <div style={{ maxWidth: "850px", margin: "30px auto", fontFamily: "Segoe UI, sans-serif", padding: "0 15px" }}>
      <Navbar />
      <ProductCatalog />
      <CartSummary />
    </div>
  );
}

Hasil Output Tampilan Aplikasi & Sinkronisasi LocalStorage

┌────────────────────────────────────────────────────────────────────────┐
│ ⚡ Zustand Mart         🛒 Keranjang: 2 Barang       Halo, Budi Santoso│
├────────────────────────────────────────────────────────────────────────┤
│ Katalog Produk Tersedia                                                │
│ ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────┐ │
│ │ Mechanical Keyboard  │ │ Wireless Mouse       │ │ Headset Gaming   │ │
│ │ Rp 850.000           │ │ Rp 450.000           │ │ Rp 650.000       │ │
│ │ [ + Masukkan Keranjang]│ │ [ + Masukkan Keranjang]│ │ [+ Masuk Keranjang]│
│ └──────────────────────┘ └──────────────────────┘ └──────────────────┘ │
├────────────────────────────────────────────────────────────────────────┤
│ Rincian Keranjang Belanja                                [Kosongkan]   │
│ • Mechanical Keyboard RGB   Rp 850.000 x 1 = Rp 850.000  [-] 1 [+] [✕] │
│ • Wireless Ergonomic Mouse  Rp 450.000 x 1 = Rp 450.000  [-] 1 [+] [✕] │
│                                                                        │
│ Total Pembayaran: Rp 1.300.000                  [ Checkout Sekarang ➡ ]│
├────────────────────────────────────────────────────────────────────────┤
│ (State otomatis tersimpan di LocalStorage key: ecommerce-zustand-store)│
└────────────────────────────────────────────────────────────────────────┘

24. 🔗 Referensi Resmi