Episode ini membahas penempatan logika di dalam store sebagai action, action async dengan async/await, pengelolaan status loading/error/success, dan error handling terpusat untuk data fetching yang rapi dan bisa diuji.

Store yang baik menyimpan tidak hanya data, tetapi juga cara data itu berubah. Episode 6 membahas dua hal: menempatkan logika update di dalam store sebagai action, dan menangani state asinkron — loading, error, dan success — dengan pola yang konsisten dan terpusat.
Aplikasi nyata hampir selalu berkomunikasi dengan server. Pola async state yang kalian kuasai di episode ini akan dipakai terus, baik langsung di store maupun terintegrasi dengan TanStack Query di episode 12.
Pindahkan logika update dari komponen ke action store. Komponen cukup memanggil action, bukan menulis ulang aturan bisnis:
type TodoState = {
todos: string[]
addTodo: (title: string) => void
removeTodo: (id: string) => void
toggleTodo: (id: string) => void
}
export const useTodo = create<TodoState>((set, get) => ({
todos: [],
addTodo: (title) =>
set((s) => ({ todos: [...s.todos, { id: crypto.randomUUID(), title, done: false }] })),
removeTodo: (id) =>
set((s) => ({ todos: s.todos.filter((t) => t.id !== id) })),
toggleTodo: (id) =>
set((s) => ({
todos: s.todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
})),
}))addTodo(title) menutup detail pembuatan object todo di dalam store, sehingga komponen tetap ramping dan logika bisa diuji tanpa UI.
Di komponen, action dipakai langsung di event handler:
const addTodo = useTodo((s) => s.addTodo)
function handleSubmit(e: FormEvent) {
e.preventDefault()
addTodo(input)
}const addTodo = useTodo((s) => s.addTodo) mengambil fungsi action sebagai selector — fungsi stabil sehingga tidak memicu re-render tambahan.
Action bisa asinkron. Pola paling umum: atur status loading, lakukan fetching, lalu set data atau error:
type UserState = {
user: User | null
status: 'idle' | 'loading' | 'success' | 'error'
error: string | null
fetchUser: (id: string) => Promise<void>
}
export const useUser = create<UserState>((set) => ({
user: null,
status: 'idle',
error: null,
fetchUser: async (id) => {
set({ status: 'loading', error: null })
try {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const user = await res.json()
set({ user, status: 'success' })
} catch (err) {
set({ status: 'error', error: (err as Error).message })
}
},
}))fetchUser(id) berjalan dalam tiga langkah: set loading, tunggu fetch, lalu set success atau error. Status string memudahkan komponen menggambar kondisi UI yang berbeda.
Gunakan status dari store untuk render bersyarat:
const { user, status, error, fetchUser } = useUser(
useShallow((s) => ({
user: s.user,
status: s.status,
error: s.error,
fetchUser: s.fetchUser,
})),
)
if (status === 'loading') return <p>Memuat...</p>
if (status === 'error') return <p>Gagal: {error}</p>
if (!user) return <button onClick={() => fetchUser('1')}>Muat profil</button>
return <p>Halo, {user.name}</p>useShallow(...) menggabungkan beberapa field dalam satu pemanggilan tanpa memicu re-render berlebihan — pola dari episode 5. UI sekarang hanya menggambar sesuai status state.
Pusatkan error handling agar semua action mengikuti aturan yang sama:
const withStatus = async <T,>(set, promise: Promise<T>): Promise<T> => {
set({ status: 'loading', error: null })
try {
const data = await promise
set({ status: 'success' })
return data
} catch (err) {
set({ status: 'error', error: (err as Error).message })
throw err
}
}
fetchUser: async (id) => {
const user = await withStatus(set, fetch(`/api/users/${id}`).then((r) => r.json()))
set({ user })
},Fungsi helper withStatus(set, promise) menormalkan siklus loading/error di satu tempat. Error tetap di-throw agar pemanggil bisa menangani efek samping lain, misalnya notifikasi atau logging ke layanan eksternal.
Gunakan AbortController untuk request yang tidak lagi relevan, misalnya saat komponen unmount:
const controller = new AbortController()
fetch('/api/users/1', { signal: controller.signal })controller.signal diteruskan ke fetch; memanggil controller.abort() menghentikan request. Detail pola ini dibahas di episode 13 bersama transient updates.
Episode 6 mengajarkan bahwa store yang baik menyimpan data sekaligus perilaku: action menampung logika update, action async mengelola loading/error/success, dan error handling terpusat membuat store konsisten serta mudah diuji.
Inti yang harus dibawa pulang:
Di episode 7 selanjutnya kita akan membahas TypeScript dan typing store — create<State>()(...) dengan generic, definisi interface state dan actions, typing middleware, serta pola combine untuk slicing store besar. TypeScript adalah perisai kalian di codebase nyata.