Episode ini membahas dua pola untuk store skala tim: custom middleware yang membungkus set untuk logging, validasi, dan tracking, serta slice pattern yang memecah store besar menjadi potongan independen. Kalian juga belajar mengetik slices agar bisa saling mengakses lewat get dengan TypeScript.

Di fase lanjutan, store mulai membutuhkan perilaku lintas-domain: log setiap perubahan, validasi payload, atau melacak aksi. Episode 16 membahas dua pola yang membuat store skala tim tetap terawat: custom middleware untuk menyisipkan logika di sekitar set, dan slice pattern untuk memecah store raksasa menjadi bagian-bagian yang independen namun tetap ter-typed.
Keduanya adalah keterampilan yang sering ditanyakan saat wawancara senior dan benar-benar dipakai di production.
Middleware Zustand adalah fungsi yang membungkus initializer store. Bentuk dasarnya menerima set, get, dan api, lalu mengembalikan set, get, dan api yang sudah dibungkus. Contoh logger yang mencatat setiap perubahan state:
import { create } from 'zustand'
const logger = (initializer) => (set, get, api) =>
initializer(
(args) => {
console.log('sebelum:', get())
set(args)
console.log('sesudah:', get())
},
get,
api,
)
export const useCounter = create(logger((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
})))logger(initializer) menerima initializer dan mengembalikan fungsi baru yang membungkus set. Setiap kali store diupdate, log mencetak state sebelum dan sesudah — tanpa mengubah API yang dipakai komponen.
Middleware berguna untuk lintas fitur yang berulang. Contoh validasi yang menolak nilai negatif, dan tracking yang mencatat aksi ke analitik:
const validate = (initializer) => (set, get, api) =>
initializer(
(args) => {
const next =
typeof args === 'function' ? args(get()) : args
if (next.count < 0) {
console.warn('count tidak boleh negatif')
return
}
set(args)
},
get,
api,
)
const track = (initializer) => (set, get, api) =>
initializer(
(args) => {
trackAnalytics('store:update', get())
set(args)
},
get,
api,
)validate menolak update yang invalid sebelum masuk ke state, sementara track mencatat aksi. Gabungkan dengan nested composition: create(validate(track(initializer))) — urutan bungkus menentukan urutan eksekusi saat update.
Ketika satu store membengkak ke puluhan action, pecah menjadi beberapa slice. Slice adalah potongan state plus action yang ditulis terpisah, lalu digabung dalam satu create:
interface UserSlice {
user: User | null
setUser: (user: User) => void
}
interface CartSlice {
items: Item[]
addItem: (item: Item) => void
}
export const useStore = create<UserSlice & CartSlice>()((...a) => ({
...createUserSlice(...a),
...createCartSlice(...a),
}))Setiap slice bisa ditulis di file sendiri: userSlice.ts dan cartSlice.ts. Store utama hanya menggabungkan hasilnya. Bila store tumbuh, tambahkan slice baru tanpa menyentuh slice lain — ini pattern yang sama dipakai di repo resmi Zustand.
Slice kadang perlu membaca state slice lain. Slice berikut mengakses user slice lewat get:
interface LoggedInSlice {
isLoggedIn: () => boolean
}
const createLoggedInSlice = (set, get) => ({
isLoggedIn: () => get().user !== null,
})
export const useStore = create<UserSlice & LoggedInSlice>()((...a) => ({
...createUserSlice(...a),
...createLoggedInSlice(...a),
}))get().user di dalam slice membaca state slice lain dengan aman karena generic gabungan memastikan tipe lengkap tersedia. Kalian tetap mendapat autocomplete dan type-check penuh di seluruh store meski state tersebar di beberapa file.
Episode 16 menambah dua alat produksi: middleware kustom yang membungkus set untuk logging, validasi, dan tracking, serta slice pattern yang memecah store besar menjadi potongan ter-typed yang saling bisa membaca lewat get.
Inti yang harus dibawa pulang:
Di episode 17 selanjutnya kita akan membahas Zustand di luar React — memakai createStore dari zustand/vanilla untuk kode non-React, membaca state dengan getState di Node service dan worker, lalu menghubungkannya kembali ke React lewat useStore.