---
name: state-management-expert
description: "Expert guide for modern client-side state management: Zustand, Jotai, Valtio, TanStack Store, Redux Toolkit, and server state patterns with TanStack Query / Panduan ahli untuk manajemen state client-side modern: Zustand, Jotai, Valtio, TanStack Store, Redux Toolkit, dan pola server state dengan TanStack Query."
author: "Roedy Rustam"
version: "4.4.2"
---
# State Management Expert (Modern React Edition)
[English](#english) | [Bahasa Indonesia](#bahasa-indonesia)
---
## English
### Orchestration & Integration
Connects and orchestrates with relevant domain skills:
- `senior-frontend` — Component architecture and avoiding prop drilling in React 19.
- `tanstack-query-expert` — Decoupling server state from client state.
- `design-system-architect` — Global UI state for theme, modals, drawers, and toasts.
- `ui-ux-pro-max` — Ephemeral UI state management and user interaction feedback.
- `visual-ui-auditor` — Multimodal vision auditing of UI state transitions and modal triggers.
- `anti-slop` — Anti-slop state architecture, preventing over-engineered stores and redundant duplicated state.
### Description
Expert guide for selecting and implementing the right state management solution for React and Next.js applications. Covers **Zustand 5**, **Jotai 2**, **Valtio**, **TanStack Store**, and **Redux Toolkit 2**, with clear guidance on when to use each, proper separation of server state (TanStack Query) from client state, and advanced patterns like slices, persistence, and devtools integration.
### Trigger Conditions
- Choosing a state management library for a new React/Next.js project.
- Refactoring prop drilling or complex Context API usage.
- Implementing global UI state (modals, drawers, theme, auth session).
- Managing complex form state across multiple steps.
- Implementing undo/redo, optimistic updates, or derived state.
- Migrating from Redux to a lighter alternative.
---
### State Category Separation (Critical First Step)
Before choosing a library, categorize your state:
| Category | Description | Solution |
|---|---|---|
| **Server State** | Data from APIs, databases | TanStack Query / SWR |
| **URL State** | Route params, search filters | `useSearchParams`, nuqs |
| **Form State** | Input values, validation errors | React Hook Form + Zod |
| **UI/Local State** | Modals, drawers, toggles | `useState` / `useReducer` |
| **Global Client State** | Theme, auth, shopping cart | Zustand / Jotai |
| **Cross-tab State** | Sync across browser tabs | Zustand + BroadcastChannel |
**Rule**: Do NOT store server data in client state. Let TanStack Query own all async data.
---
### Library Selection Guide
| Criteria | Zustand 5 | Jotai 2 | Valtio | Redux Toolkit |
|---|---|---|---|---|
| **Mental Model** | Single store, actions | Atomic, bottom-up | Proxy-based, mutable | Flux, reducers |
| **Bundle Size** | ~1KB | ~3KB | ~3KB | ~15KB+ |
| **DevTools** | Redux DevTools | Jotai DevTools | Redux DevTools | Built-in |
| **Async** | Manual or middleware | `atomWithQuery` | `snapshot` | `createAsyncThunk` |
| **Best For** | General global state | Atomic derived state | Simple mutable state | Large teams, strict patterns |
**Recommendation**: Use **Zustand** as the default. Use **Jotai** for fine-grained reactive atoms. Use **Redux Toolkit** only for large enterprise teams with strict conventions.
---
### Zustand 5 — Best Practices
#### Basic Store with TypeScript
```typescript
// stores/ui.store.ts
import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface UiState {
theme: 'light' | 'dark' | 'system';
sidebarOpen: boolean;
activeModal: string | null;
setTheme: (theme: UiState['theme']) => void;
toggleSidebar: () => void;
openModal: (modalId: string) => void;
closeModal: () => void;
}
export const useUiStore = create()(
devtools(
persist(
subscribeWithSelector(
immer((set) => ({
theme: 'system',
sidebarOpen: true,
activeModal: null,
setTheme: (theme) => set((state) => { state.theme = theme; }),
toggleSidebar: () => set((state) => { state.sidebarOpen = !state.sidebarOpen; }),
openModal: (modalId) => set((state) => { state.activeModal = modalId; }),
closeModal: () => set((state) => { state.activeModal = null; }),
}))
),
{ name: 'ui-store', partialize: (s) => ({ theme: s.theme }) }
),
{ name: 'UiStore' }
)
);
```
#### Slice Pattern for Large Stores
```typescript
// Pattern: separate slices, combine into one store
import { StateCreator } from 'zustand';
interface AuthSlice {
user: User | null;
isAuthenticated: boolean;
login: (user: User) => void;
logout: () => void;
}
interface CartSlice {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: () => number;
}
type AppStore = AuthSlice & CartSlice;
const createAuthSlice: StateCreator = (set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
});
const createCartSlice: StateCreator = (set, get) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
removeItem: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
total: () => get().items.reduce((sum, item) => sum + item.price * item.qty, 0),
});
export const useAppStore = create()(
devtools((...args) => ({
...createAuthSlice(...args),
...createCartSlice(...args),
}))
);
```
#### Selective Subscriptions (Prevent Unnecessary Rerenders)
```typescript
// Only re-render when `theme` changes, not the entire store
const theme = useUiStore((s) => s.theme);
const isModalOpen = useUiStore((s) => s.activeModal === 'confirm-delete');
// Subscribe outside React (side effects, non-component code)
useUiStore.subscribe(
(s) => s.theme,
(theme) => document.documentElement.dataset.theme = theme,
{ fireImmediately: true }
);
```
---
### Jotai 2 — Atomic State Patterns
```typescript
// atoms/user.atoms.ts
import { atom, loadable } from 'jotai';
import { atomWithStorage, atomWithReset } from 'jotai/utils';
// Primitive atom
export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');
// Derived (read-only) atom
export const isDarkAtom = atom((get) => get(themeAtom) === 'dark');
// Async atom with loadable for Suspense-free usage
export const userAtom = atom(async () => {
const res = await fetch('/api/me');
if (!res.ok) throw new Error('Failed to fetch user');
return res.json() as Promise;
});
export const loadableUserAtom = loadable(userAtom);
// In component:
function UserWidget() {
const loadableUser = useAtomValue(loadableUserAtom);
if (loadableUser.state === 'loading') return ;
if (loadableUser.state === 'hasError') return ;
return {loadableUser.data.name}
;
}
```
---
### Redux Toolkit 2 — Modern Patterns
```typescript
// features/cart/cartSlice.ts
import { createSlice, PayloadAction, createSelector } from '@reduxjs/toolkit';
interface CartState {
items: CartItem[];
status: 'idle' | 'loading' | 'failed';
}
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], status: 'idle' } as CartState,
reducers: {
addItem(state, action: PayloadAction) {
state.items.push(action.payload); // Immer handles immutability
},
removeItem(state, action: PayloadAction) {
state.items = state.items.filter((i) => i.id !== action.payload);
},
},
});
// Memoized selector
export const selectCartTotal = createSelector(
(state: RootState) => state.cart.items,
(items) => items.reduce((sum, i) => sum + i.price * i.qty, 0)
);
export const { addItem, removeItem } = cartSlice.actions;
export default cartSlice.reducer;
```
---
### Integration with Next.js App Router
- **Server Components**: No client state. Fetch directly.
- **Client Components**: Use Zustand/Jotai wrapped in providers.
- **Hydration**: Initialize Zustand store from server-fetched data using `useHydrateAtoms` (Jotai) or a `HydrationBoundary` pattern.
```typescript
// providers/store-provider.tsx (App Router pattern)
'use client';
import { createStore, Provider } from 'jotai';
import { useRef } from 'react';
type Props = { children: React.ReactNode; initialUser?: User };
export function StoreProvider({ children, initialUser }: Props) {
const store = useRef(createStore());
if (initialUser) store.current.set(userAtom, initialUser);
return {children};
}
```
### Sovereign Execution, Token Efficiency & Invariants
| Dimension | Sovereign Mandate & Deterministic Contract |
| :--- | :--- |
| **Frontier Model Cascade** | Medium / High (`Gemini 4 Argon`, `Claude Sonnet 5.5 High`, `Muse Wonder High`, `Composer 2.5 Fast`) |
| **Availability Fallback** | `Gemini 4 Argon/Pro` -> `Gemini 3.8 Flash`; `Claude Opus 5.5` -> `Claude Opus 4.6` (Zero unhandled missing-model errors) |
| **KV-Cache & Token Protocol** | Pin static instructions & schemas at prompt prefix (`kv-cache-prefix-optimizer`); pass surgical `CONTEXT_MAP.md` slices (<250 tokens/call) during Swarm Fan-Out |
| **SaaT CLI & MCP Dispatch** | CLI: `vibes skill exec state-management-expert ` / MCP: `vibes_skill_dispatch`, `vibes_visual_audit`, `vibes_ast_query`, `vibes_verify_gate` |
| **NFR & Anti-Slop Invariants** | LCP < 1.8s, INP < 100ms, CLS < 0.05, 1px crisp borders, 3-tier radius (6px/8px/12px), `tabular-nums`, 5-state interactive contract; 100% complete zero-placeholder output verified via `node scripts/check-anti-slop.js --strict` |
---
---
## Bahasa Indonesia
### Integrasi Orkestrasi
Terhubung dan mengorkestrasi skill domain yang relevan:
- `senior-frontend` — Arsitektur komponen dan eliminasi prop drilling pada React 19.
- `tanstack-query-expert` — Pemisahan server state dari client state secara bersih.
- `design-system-architect` — State UI global untuk tema, modal, drawer, dan toast.
- `ui-ux-pro-max` — Manajemen state interaksi UI dan respons antarmuka pengguna.
- `visual-ui-auditor` — Audit visual multimodal transisi state UI dan trigger modal.
- `anti-slop` — Arsitektur state anti-slop, mencegah store berlebihan (over-engineered) dan duplikasi state.
### Deskripsi
Panduan ahli untuk memilih dan mengimplementasikan solusi manajemen state yang tepat untuk aplikasi React 19 dan Next.js 15. Mencakup **Zustand 5**, **Jotai 2**, **Valtio**, dan **Redux Toolkit 2**, dengan panduan pemilihan yang jelas, pemisahan server state (TanStack Query) dari client state, dan pola lanjutan seperti slices, persistensi, dan integrasi devtools.
### Kondisi Pemicu
- Memilih library state management untuk proyek React/Next.js baru.
- Refaktor prop drilling atau penggunaan Context API yang rumit.
- Mengimplementasikan global UI state (modal, drawer, tema, sesi auth).
- Mengelola state form kompleks di beberapa langkah.
- Mengimplementasikan undo/redo, optimistic update, atau derived state.
- Migrasi dari Redux ke alternatif yang lebih ringan.
---
### Pemisahan Kategori State (Prinsip Terpenting)
| Kategori | Definisi | Solusi Standar 2026 |
|---|---|---|
| **Server State** | Data yang berasal dari database atau endpoint API | TanStack Query v5 / SWR |
| **URL State** | Filter pencarian, pagination, parameter routing | `useSearchParams`, `nuqs` |
| **Form State** | Input formulir, status validasi, pesan kesalahan | React Hook Form + Zod |
| **UI Local State** | Buka/tutup modal, dropdown, toggle accordion | `useState` / `useReducer` |
| **Global Client State** | Status autentikasi, tema, preferensi UI, keranjang belanja | Zustand 5 / Jotai 2 |
**Aturan Emas**: Jangan pernah menyimpan salinan data server di dalam store klien. Serahkan seluruh sinkronisasi data async ke TanStack Query.
---
### Pola Kunci Zustand 5 (TypeScript)
#### 1. Store Global dengan Middleware DevTools & Persistensi
```typescript
// stores/ui.store.ts
import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface UiState {
tema: 'terang' | 'gelap' | 'sistem';
sidebarBuka: boolean;
modalAktif: string | null;
aturTema: (tema: UiState['tema']) => void;
toggleSidebar: () => void;
bukaModal: (idModal: string) => void;
tutupModal: () => void;
}
export const useUiStore = create()(
devtools(
persist(
subscribeWithSelector(
immer((set) => ({
tema: 'sistem',
sidebarBuka: true,
modalAktif: null,
aturTema: (tema) => set((draft) => { draft.tema = tema; }),
toggleSidebar: () => set((draft) => { draft.sidebarBuka = !draft.sidebarBuka; }),
bukaModal: (idModal) => set((draft) => { draft.modalAktif = idModal; }),
tutupModal: () => set((draft) => { draft.modalAktif = null; }),
}))
),
{ name: 'penyimpanan-ui', partialize: (s) => ({ tema: s.tema }) }
),
{ name: 'UiStore' }
)
);
```
#### 2. Selective Subscriptions untuk Mencegah Re-render Berlebih
```typescript
// Hanya komponen yang membutuhkan tema yang akan re-render saat tema berubah
const tema = useUiStore((s) => s.tema);
const isModalHapusBuka = useUiStore((s) => s.modalAktif === 'konfirmasi-hapus');
// Langganan di luar siklus React untuk manipulasi DOM langsung
useUiStore.subscribe(
(s) => s.tema,
(tema) => { document.documentElement.dataset.theme = tema; },
{ fireImmediately: true }
);
```
#### 3. Provider Jotai 2 pada Next.js App Router
```typescript
// providers/store-provider.tsx
'use client';
import { createStore, Provider } from 'jotai';
import { useRef } from 'react';
export function StoreProvider({ children }: { children: React.ReactNode }) {
const storeRef = useRef(createStore());
return {children};
}
```
### Matriks Eksekusi Berdaulat, Efisiensi Token & Invarian
| Dimensi | Mandat Berdaulat & Kontrak Deterministik |
| :--- | :--- |
| **Kaskade Model Frontier** | Medium / High (`Gemini 4 Argon`, `Claude Sonnet 5.5 High`, `Muse Wonder High`, `Composer 2.5 Fast`) |
| **Invarian Fallback Ketersediaan** | `Gemini 4 Argon/Pro` -> `Gemini 3.8 Flash`; `Claude Opus 5.5` -> `Claude Opus 4.6` (Bebas gagal saat model belum tersedia/kuota penuh) |
| **Protokol Hemat Token & KV-Cache** | Kunci instruksi & skema statis di prefix awal (`kv-cache-prefix-optimizer`); kirim potongan `CONTEXT_MAP.md` (<250 token/panggilan) saat Fan-Out |
| **Eksekusi SaaT CLI & MCP** | CLI: `vibes skill exec state-management-expert ` / MCP: `vibes_skill_dispatch`, `vibes_visual_audit`, `vibes_ast_query`, `vibes_verify_gate` |
| **Invarian NFR & Anti-Slop** | LCP < 1.8s, INP < 100ms, CLS < 0.05, border tegas 1px, radius 3-tier (6px/8px/12px), `tabular-nums`, kontrak interaktif 5-state; kode 100% tuntas tanpa placeholder diverifikasi melalui `node scripts/check-anti-slop.js --strict` |