Belajar Redux - Middleware: Thunk & createListenerMiddleware
Episode 12 of 23

Belajar Redux - Middleware: Thunk & createListenerMiddleware

Episode ini membahas middleware Redux: Thunk sebagai fondasi createAsyncThunk, kapan menulis thunk manual, serta createListenerMiddleware untuk merespons action secara terpusat dengan log, side-effect, dan debounce tanpa mencemari reducer.

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

Pendahuluan

Sejauh ini semua state berubah lewat action yang langsung dikirim ke reducer. Tetapi bagaimana kalau kita butuh efek samping: menyimpan ke localStorage, memanggil API sebelum dispatch, atau menunggu beberapa saat sebelum merespons? Reducer harus tetap murni, jadi efek samping ditangani di lapisan lain — itulah middleware.

Episode 12 membedah dua middleware penting. Pertama Redux Thunk, yang menjadi fondasi createAsyncThunk. Kedua createListenerMiddleware, alat modern untuk merespons action secara terpusat dengan debounce, logging, dan side-effect tanpa menulis kode di dalam reducer.

Memahami Redux Thunk

Mengapa Thunk Ada

Thunk adalah fungsi yang membungkus operasi tertunda. Redux Thunk memungkinkan kita dispatch sebuah fungsi — bukan hanya action objek. Fungsi itu menerima dispatch dan getState, sehingga bisa menunda dispatch sampai operasi async selesai:

JSThunk manual untuk login
export const loginWithEmail = (email: string, password: string) =>
  async (dispatch, getState) => {
    dispatch(loginPending())
    try {
      const res = await fetch("/api/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password }),
      })
      const user = await res.json()
      dispatch(loginSucceeded(user))
    } catch (error) {
      dispatch(loginFailed(String(error)))
    }
  }

Middleware thunk mendeteksi bahwa nilai yang di-dispatch adalah fungsi, memanggilnya dengan dispatch dan getState, lalu mengembalikan hasilnya. Dengan pola ini seluruh logika async berada di satu tempat yang bisa di-test.

Kapan Menulis Thunk Manual

createAsyncThunk menangani pola paling umum: satu action pending, fulfilled, rejected. Namun ada kasus yang tetap lebih baik ditulis manual:

  • Alur yang membutuhkan beberapa dispatch berurutan dengan aksi lokal.
  • Perlu mengakses getState() untuk memutuskan apakah request perlu dikirim.
  • Menggabungkan beberapa API call dalam satu alur transaksional.

Contoh alur ganda yang lebih ekspresif sebagai thunk manual:

JSThunk manual dengan beberapa dispatch
export const checkout = (items: CartItem[]) => async (dispatch, getState) => {
  const { auth } = getState()
  if (!auth.token) {
    dispatch(promptLogin())
    return
  }
  dispatch(orderPending())
  const order = await createOrder(items, auth.token)
  dispatch(orderCreated(order))
  dispatch(cartCleared())
  trackEvent("checkout_completed", { itemCount: items.length })
}

createAsyncThunk sebagai Thunk Modern

Struktur di Balik Lifecycle

Sebenarnya createAsyncThunk membungkus logika thunk manual. Ia membuat thunk yang memdispatch tiga action pembungkus — pending, fulfilled, rejected — sehingga reducer bisa merespons setiap tahap dengan extraReducers:

JScreateAsyncThunk dan extraReducers
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
 
export const fetchUsers = createAsyncThunk(
  "users/fetchUsers",
  async () => {
    const res = await fetch("/api/users")
    if (!res.ok) throw new Error("Gagal memuat users")
    return res.json()
  },
)
 
const usersSlice = createSlice({
  name: "users",
  initialState: { items: [] as string[], loading: false },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.items = action.payload
        state.loading = false
      })
      .addCase(fetchUsers.rejected, (state) => {
        state.loading = false
      })
  },
})

Ketiga action otomatis tersedia sebagai properti thunk: fetchUsers.pending, fetchUsers.fulfilled, fetchUsers.rejected. Aturan praktis: gunakan createAsyncThunk untuk satu request sederhana, thunk manual untuk alur multi-langkah.

createListenerMiddleware

Reaksi Terpusat Tanpa Mencemari Reducer

Listener middleware menangkap action dan menjalankan efek samping di satu tempat. Tambahkan ke store lalu daftarkan listener dengan startListening:

JSsrc/app/listenerMiddleware.ts
import { createListenerMiddleware } from "@reduxjs/toolkit"
import { logAdded } from "../features/logger/loggerSlice"
 
export const listenerMiddleware = createListenerMiddleware()
 
listenerMiddleware.startListening({
  matcher: (action) => action.type.endsWith("/rejected"),
  effect: async (action, listenerApi) => {
    console.error("Action gagal:", action.type, action.error)
    listenerApi.dispatch(logAdded(`${action.type} rejected`))
  },
})

Listener ini menangkap semua action rejected dari thunk dan mencatatnya. listenerApi menyediakan dispatch, getState, cancelActiveListeners, serta delay untuk timing.

Debounce dan Side-effect

createListenerMiddleware dirancang untuk kasus yang membutuhkan delay, seperti debounce pencarian yang hanya memicu request setelah user berhenti mengetik:

JSDebounce pencarian dengan listener
import { createListenerMiddleware } from "@reduxjs/toolkit"
import { searchQueryChanged } from "../features/search/searchSlice"
import { fetchResults } from "../features/search/searchSlice"
 
export const searchListener = createListenerMiddleware()
 
searchListener.startListening({
  actionCreator: searchQueryChanged,
  effect: async (action, listenerApi) => {
    listenerApi.cancelActiveListeners()
    await listenerApi.delay(400)
    listenerApi.dispatch(fetchResults(action.payload))
  },
})

Setiap kali user mengetik, listener membatalkan listener sebelumnya lalu menunggu 400 ms. cancelActiveListeners dan delay adalah pasangan utama untuk timing — request hanya dikirim setelah jeda, dan reducer tetap bersih karena seluruh logika timing ada di middleware.

Tip

Jangan lupa menambahkan middleware ke store: getDefaultMiddleware().prepend(listenerMiddleware.middleware). Gunakan prepend agar listener menangkap action sebelum listener lain memprosesnya.

Penutup

Middleware adalah tempat efek samping hidup, dan Redux Toolkit menyediakan dua cara: Thunk untuk alur async berbasis fungsi, serta createListenerMiddleware untuk reaksi terpusat terhadap action. Thunk cocok untuk operasi yang butuh kontrol penuh atas dispatch berurutan, sedangkan listener unggul untuk logging, debounce, dan sinkronisasi lintas fitur — semuanya tanpa mengotori reducer murni.

Inti yang harus dibawa pulang:

  • Thunk adalah fungsi yang menerima dispatch dan getState, dijalankan oleh middleware Redux Thunk.
  • createAsyncThunk membungkus thunk manual untuk pola satu request dengan lifecycle pending, fulfilled, rejected.
  • Thunk manual tetap tepat untuk alur multi-dispatch dan keputusan berbasis getState.
  • createListenerMiddleware menjalankan efek samping saat action tertentu lewat.
  • matcher dan actionCreator menentukan action mana yang ditangkap.
  • cancelActiveListeners plus delay menghasilkan debounce tanpa kode di reducer.

Episode 13 berikutnya mengalihkan perhatian ke performansi — kalian akan memahami reselect dan createSelector untuk memoization, komposisi derived state, menghindari re-render berlebihan, serta teknik profiling dengan Redux DevTools.