Belajar Zustand - Actions & Async State
Episode 6 of 23

Belajar Zustand - Actions & Async State

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Pendahuluan

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.

Actions dalam Store

Menempatkan Logika di Dalam Store

Pindahkan logika update dari komponen ke action store. Komponen cukup memanggil action, bukan menulis ulang aturan bisnis:

JSAction di dalam store
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.

Update dari Event Handler

Di komponen, action dipakai langsung di event handler:

JSAction 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.

Async State dan Data Fetching

Action Async dengan async/await

Action bisa asinkron. Pola paling umum: atur status loading, lakukan fetching, lalu set data atau error:

JSAction async untuk fetch data
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.

Status loading/error/success di Komponen

Gunakan status dari store untuk render bersyarat:

JSRender berdasarkan status
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.

Error Handling Terpusat

Satu Pola Error untuk Semua Action

Pusatkan error handling agar semua action mengikuti aturan yang sama:

JSHelper async status
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.

Jangan Lupa Membatalkan Request Kedaluwarsa

Gunakan AbortController untuk request yang tidak lagi relevan, misalnya saat komponen unmount:

JSAbort request yang tidak perlu
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.

Penutup

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:

  • Tempatkan logika update di action store, bukan di komponen.
  • Action async memakai async/await dengan status idle/loading/success/error.
  • Render UI bersyarat berdasarkan status untuk loading, error, dan data.
  • Pusatkan error handling dalam satu helper agar semua action konsisten.
  • Selalu reset error saat memulai request baru.
  • AbortController mencegah update state dari request yang sudah basi.

Di episode 7 selanjutnya kita akan membahas TypeScript dan typing storecreate<State>()(...) dengan generic, definisi interface state dan actions, typing middleware, serta pola combine untuk slicing store besar. TypeScript adalah perisai kalian di codebase nyata.

Belajar Zustand - Actions & Async State | Belajar Zustand