Episode ini membahas persistence dan rehydration lanjutan: custom storage dengan createJSONStorage untuk sessionStorage dan backend, serialization dan deserialization, serta versioning dengan migrate untuk memigrasikan state lama. Kalian juga belajar menangani state yang gagal ter-rehydrate lewat callback onRehydrateStorage.

Episode 8 memperkenalkan middleware persist dengan localStorage bawaan. Episode 11 membawanya lebih jauh: bagaimana menyimpan state ke storage yang bukan localStorage, bagaimana mengontrol proses serialization, dan bagaimana memigrasikan state lama ketika bentuk data berubah seiring versi aplikasi. Ini adalah bekal yang wajib kalian miliki sebelum aplikasi dinaikkan ke production.
Tiga topik utama yang kita bedah: custom storage dengan createJSONStorage untuk sessionStorage hingga endpoint backend, opsi version plus migrate untuk state yang berubah skema, dan callback onRehydrateStorage untuk menangani kegagalan saat state dimuat ulang dari penyimpanan.
Secara default, persist memakai localStorage. Untuk mengubah tujuan penyimpanan, createJSONStorage menghasilkan adapter storage yang memakai getItem, setItem, dan removeItem standar:
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'
interface SessionState {
token: string
setToken: (token: string) => void
}
export const useSession = create<SessionState>()(
persist(
(set) => ({
token: '',
setToken: (token) => set({ token }),
}),
{
name: 'session-store',
storage: createJSONStorage(() => sessionStorage),
},
),
)createJSONStorage(() => sessionStorage) menerima fungsi yang mengembalikan objek Storage. Mengapa fungsi, bukan objek langsung? Karena di lingkungan seperti server, sessionStorage tidak tersedia saat modul dimuat — dengan fungsi, akses ditunda sampai benar-benar dibutuhkan.
Storage tidak harus berupa penyimpanan browser. Zustand mendukung storage asinkron apa pun yang mengimplementasikan kontrak getItem, setItem, dan removeItem. Untuk menyimpan state ke API backend:
const apiStorage = {
getItem: async (name: string) => {
const res = await fetch(`/api/storage/${name}`)
return res.json()
},
setItem: async (name: string, value: string) => {
await fetch(`/api/storage/${name}`, {
method: 'POST',
body: value,
})
},
removeItem: async (name: string) => {
await fetch(`/api/storage/${name}`, { method: 'DELETE' })
},
}Persist menyimpan state sebagai string JSON. Secara default, hanya nilai yang didukung JSON yang ikut tersimpan — fungsi seperti action di dalam store otomatis dihilangkan. Untuk format non-JSON, kalian bisa menimpa perilaku default lewat opsi serialize dan deserialize:
persist(initializer, {
name: 'cart',
storage: createJSONStorage(() => localStorage, {
reviver: (key, value) => (key === 'total' ? Number(value) : value),
replacer: (key, value) => (key === 'total' ? value.toFixed(2) : value),
}),
})replacer dan reviver mengikuti kontrak JSON.stringify dan JSON.parse. Gunakan ini untuk mengonversi tipe khusus seperti Date atau BigInt yang tidak didukung JSON asli. Untuk kebutuhan di luar JSON, tulis storage kustom yang mengembalikan string mentah.
Skema state selalu bisa berubah. Ketika kalian menambah field baru atau mengubah bentuk field lama, state yang sudah tersimpan di storage pengguna menjadi usang. Opsi version menandai skema state:
export const useSettings = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{ name: 'app-settings', version: 2 },
),
)version: 2 memberi tahu persist bahwa state hanya valid untuk skema versi 2. Ketika kalian menaikkan angka ini, persist tahu bahwa state lama perlu diproses sebelum dipakai.
Menyandingkan version dengan migrate untuk mengonversi state lama ke bentuk baru:
migrate: (persisted, version) => {
if (version === 0) {
return { ...persisted, theme: 'light', fontSize: 14 }
}
if (version === 1) {
return { ...persisted, fontSize: 14 }
}
return persisted
},migrate(persisted, version) menerima state yang terbaca dan nomor versinya. Fungsi ini berjalan sekali saat rehydrate, lalu hasilnya menjadi initial state store. Tanpa migrate, state yang versinya tidak dikenal akan diabaikan.
Proses memuat ulang state dari storage disebut rehydration. Untuk storage asinkron, proses ini bisa gagal atau selesai setelah render pertama. Callback onRehydrateStorage memberi kalian titik masuk untuk menanganinya:
onRehydrateStorage: () => (state, error) => {
if (error) {
console.error('Rehydration gagal', error)
return
}
console.log('State dimuat dari storage', state)
},onRehydrateStorage: () => (state, error) => ... dipanggil saat proses rehydrate selesai. Ini tempat yang tepat untuk logging, sinkronisasi state ke analitik, atau menampilkan notifikasi ketika state pengguna dipulihkan. Kombinasikan dengan pola skipHydration dari episode 8 untuk kontrol penuh atas waktu rehydration.
Episode 11 menutup lapisan persistence lanjutan: storage di luar localStorage lewat createJSONStorage, serialization kustom untuk tipe non-JSON, versioning dengan migrate untuk skema yang berubah, dan callback onRehydrateStorage untuk kegagalan rehydrate.
Inti yang harus dibawa pulang:
Di episode 12 selanjutnya kita akan membahas server state dan integrasi data — membedakan data API dari state UI, kapan data masuk ke Zustand dan kapan ke TanStack Query, serta pola hybrid auth dan user profile.