12. State Management: Client State Deep Dive
Dalam arsitektur aplikasi frontend modern, Client State merujuk pada state lokal yang hidup sepenuhnya di sisi klien (browser) untuk mengontrol perilaku antarmuka pengguna (UI). Berbeda dengan Server State, Client State tidak memerlukan sinkronisasi data API secara konstan, melainkan melayani status visual seperti modal open/close, tema (dark/light), langkah form wizard, keranjang belanja lokal, status sidebar, atau status otentikasi user sementara.
Untuk mengelola Client State, terdapat beberapa pendekatan populer dengan keunggulan dan kekurangannya masing-masing.
A. Zustand (Rekomendasi Utama)
Pustaka manajemen state berbasis hook yang sangat ringan, cepat, dan intuitif. Zustand mempermudah pembuatan store global terpusat tanpa memaksakan boilerplate atau penggunaan Context Provider.
- Karakteristik:
- Global store terpusat, tetapi dikonsumsi sebagai custom hook.
- Pendekatan berlangganan (subscription-based) yang meminimalkan render ulang.
- Sangat ramah terhadap TypeScript dan performa runtime browser.
🟢 Keunggulan (Advantages)
- Zero Boilerplate & Setup Cepat: Pendefinisian store sangat ringkas melalui fungsi
create. Tidak memerlukan setup Provider pembungkus di root aplikasi, Actions, Reducers, atau Types yang berlebihan. - Sangat Ringan (Ultra-lightweight): Ukuran bundel sangat kecil (~1.5kB gzipped), menjadikannya salah satu pilihan paling efisien untuk performa load awal.
- Performa Tinggi (Selective Re-rendering): Menggunakan selector secara eksplisit saat mengakses state, sehingga komponen hanya akan dirender ulang jika bagian data spesifik yang di-subscribe berubah.
- Akses di Luar React (Non-React API Access): State dan fungsi action dapat dibaca dan dimodifikasi secara langsung dari luar komponen React (misalnya di dalam interceptor Axios atau middleware routing).
- Middleware Bawaan yang Matang: Dilengkapi dengan utility out-of-the-box seperti
persist(untuk auto-save state ke localStorage/sessionStorage) dandevtools(integrasi dengan Redux DevTools).
🔴 Kekurangan (Disadvantages)
- Struktur Kurang Kaku: Fleksibilitas tinggi dapat memicu ketidakkonsistenan penulisan kode di tim besar jika tidak ada panduan gaya (style guide) internal yang jelas.
- Kurang Cocok untuk State Granular Masif: Untuk data UI yang sangat dinamis dengan pembaruan frekuensi tinggi pada ratusan elemen independen, model centralized store di Zustand terkadang membutuhkan optimasi selector yang rumit.
B. Redux Toolkit (RTK)
Opsi enterprise standard yang membungkus Redux tradisional untuk mengurangi boilerplate. RTK memaksa struktur kode yang sangat disiplin dan searah (unidirectional data flow) untuk mengelola state global dalam skala masif.
- Karakteristik:
- Store global tunggal (Single Source of Truth) yang terpusat secara mutlak.
- Alur data searah yang sangat terdefinisi (Action -> Dispatcher -> Reducer -> Store -> View).
- Menggunakan library
Immeruntuk mempermudah mutasi state yang immutable.
🟢 Keunggulan (Advantages)
- Standardisasi & Konsistensi Kode: Memaksa pola desain yang sama di seluruh tim. Ini sangat menguntungkan pada proyek skala enterprise dengan puluhan developer.
- Debugging yang Sangat Powerful (Time-Travel): Ekosistem Redux DevTools adalah yang terbaik di industri, memungkinkan developer melacak setiap action, melihat diff state secara visual, dan memutar balik waktu (time-travel debugging).
- Middleware & Side-Effects Terintegrasi: Memiliki dukungan bawaan untuk asynchronous logic (Redux Thunk) serta kompatibilitas penuh dengan middleware canggih seperti Redux Saga atau Redux Observable.
- RTK Query: Menyertakan modul caching data server yang matang untuk memangkas boilerplate data fetching.
🔴 Kekurangan (Disadvantages)
- Boilerplate Tinggi: Walaupun jauh lebih ringkas dari Redux lama, RTK masih menuntut penulisan kode yang panjang (seperti slice, action, reducer, configure store, dan custom selector).
- Ukuran Bundel Besar: Footprint library cukup besar (~15kB gzipped) dan dapat terus bertambah jika dipadukan dengan pustaka ekosistem Redux lainnya.
- Kurva Belajar Curam (Learning Curve): Kompleks bagi developer junior untuk memahami alur dispatching, reducer, middleware, dan konsep immutability.
- Wajib Provider Wrapper: Mengharuskan pembungkusan root aplikasi dengan
<Provider store={store}>yang menambah overhead inisialisasi.
C. Jotai
Pustaka state management minimalis yang mengusung konsep Atomic State (terinspirasi dari Facebook Recoil). Jotai memecah state global menjadi unit-unit terkecil (atoms) dan menyusunnya dari bawah ke atas (bottom-up).
- Karakteristik:
- State dikelola dalam bentuk atom-atom kecil yang terpisah.
- Komponen React dapat mengonsumsi atom menggunakan hook mirip
useState. - Atom dapat saling bergantung dan menurunkan nilai baru (derived atoms).
🟢 Keunggulan (Advantages)
- Sangat Granular & Efisien: Re-render hanya terjadi secara lokal pada komponen yang berlangganan atom tertentu saja. Sangat minim render ulang yang tidak perlu.
- Performa Ekstrem untuk Dynamic UI: Pilihan terbaik untuk aplikasi interaktif seperti editor gambar/grafis, canvas, spreadsheet, atau UI builder yang memiliki ribuan elemen interaktif yang berubah secara dinamis.
- API Sederhana & Ramah: Penggunaan hook
useAtomsangat mirip dengan standarduseStatebawaan React, sehingga mudah dipelajari. - Ukuran Bundel Ringan: Sangat kecil (~3kB gzipped) dengan minimal overhead.
- Provider-less by Default: Tidak wajib dibungkus dengan Provider di root, namun mendukung multiple Providers jika ingin mengisolasi state per sub-tree.
🔴 Kekurangan (Disadvantages)
- Struktur Global yang Tersebar: Kurang cocok untuk mengelola data global terpusat yang masif (seperti log aktivitas pengguna atau session state) karena lokasinya yang tersebar secara "atomik".
- Ketergantungan Atom yang Rumit: Jika tidak direncanakan secara matang, relasi antar derived atoms (atom yang nilainya dihitung dari atom lain) bisa menjadi kusut dan sulit di-debug.
- Ekosistem Tambahan yang Terbatas: Pustaka ekosistem dan middleware pendukungnya tidak sekomprehensif Zustand atau Redux.
D. Context API
Fitur bawaan React untuk meneruskan data ke komponen anak tanpa harus melakukan manual passing props (prop drilling).
- Karakteristik:
- Menggunakan native API dari React (
createContext,useContext). - Menyebarkan state dari atas ke bawah (top-down) melalui Provider.
- Tidak menambah ukuran bundle project karena tidak memerlukan dependency eksternal.
- Menggunakan native API dari React (
🟢 Keunggulan (Advantages)
- Zero Dependency: Karena bawaan dari React, tidak menambah ukuran bundle size dan tidak memiliki risiko keamanan dependency pihak ketiga.
- Sangat Mudah Digunakan: Menggunakan konsep React native yang sudah diketahui hampir seluruh developer React.
- Sangat Cocok untuk Data Statis: Solusi ideal untuk membagikan data global yang jarang berubah seperti tema aplikasi (dark/light mode), pengaturan bahasa (locale ID/EN), atau konfigurasi static global.
🔴 Kekurangan (Disadvantages)
- Bencana Performa pada State Dinamis: Masalah utama Context API adalah ketika value dalam Provider berubah, seluruh komponen anak yang mengonsumsi Context tersebut akan di-render ulang secara default, terlepas dari apakah mereka menggunakan bagian data yang berubah itu atau tidak.
- Provider Hell (Tumpukan Wrapper): Jika Anda memecah context menjadi banyak file untuk menghindari masalah performa, Anda akan berakhir dengan struktur kode root yang dipenuhi tumpukan
<Provider>(nested Provider hell). - Tidak Cocok untuk High-Frequency Updates: Sangat tidak direkomendasikan untuk data yang nilainya sering berubah (misal: input teks real-time, counter, status progress) karena akan menyebabkan stuttering/lag pada UI.
Tabel Perbandingan Client State Tools
| Kriteria | Zustand | Redux Toolkit (RTK) | Jotai | Context API |
|---|---|---|---|---|
| Paradigma | Centralized Store (Hook-based) | Centralized Store (Slice-based) | Atomic State (Bottom-up) | Context Provider (Top-down) |
| Ukuran Bundel | Sangat Ringan (~1.5kB) | Cukup Berat (~15kB) | Ringan (~3kB) | Zero (Bawaan React) |
| Boilerplate | Sangat Rendah | Tinggi | Sangat Rendah | Rendah |
| Performa (Selective Re-render) | Sangat Baik (via Selectors) | Baik (via Reselect/Selectors) | Sangat Baik (Atomic level) | Buruk (Memicu re-render massal) |
| Kurva Belajar | Sangat Mudah | Curam / Sulit | Mudah | Sangat Mudah |
| Akses di Luar React | Ya (Sangat Mudah) | Ya (via Store Dispatch) | Cukup Sulit | Tidak Bisa |
| Persistensi / DevTools | Bawaan & Mudah | Sangat Matang (DevTools Terbaik) | Didukung (Terbatas) | Harus dibuat manual |
Panduan Memilih Client State (Decision Guide)
-
Gunakan Zustand jika:
- Anda ingin mengelola state global umum (seperti cart, status sidebar, settings) dengan cepat, bersih, dan performa tinggi tanpa boilerplate.
- Ini merupakan standard default yang paling direkomendasikan untuk proyek aplikasi web berskala kecil hingga menengah-besar.
-
Gunakan Redux Toolkit (RTK) jika:
- Anda membangun aplikasi tingkat enterprise berskala sangat masif dengan banyak tim.
- Dibutuhkan debugging visual yang mendalam (Time-travel) dan standarisasi kode yang kaku.
- Terdapat side-effects asinkronus yang sangat kompleks dan berantai.
-
Gunakan Jotai jika:
- Anda sedang membangun UI dinamis yang interaktif secara granular (seperti canvas drawing, interactive builders, map dashboards).
- Anda memerlukan performa update yang sangat terisolasi per komponen kecil.
-
Gunakan Context API jika:
- Anda hanya perlu membagikan data global yang nilainya bersifat statis atau jarang sekali berubah (seperti tema gelap/terang, locale bahasa, atau config static global).
- Anda tidak ingin menginstal library pihak ketiga tambahan.
Contoh Client State (Zustand):
import { create } from "zustand";
interface AuthState {
user: any | null;
setUser: (user: any) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}));
Contoh Client State (Redux Toolkit):
import { createSlice, configureStore } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1;
},
},
});
export const store = configureStore({ reducer: counterSlice.reducer });
Contoh Client State (Jotai):
import { atom, useAtom } from "jotai";
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Contoh Client State (Context API):
import React, { createContext, useContext, useState, ReactNode } from "react";
interface ThemeContextType {
theme: "light" | "dark";
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<"light" | "dark">("light");
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme harus digunakan di dalam ThemeProvider");
}
return context;
}