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.

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.
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:
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.
createAsyncThunk menangani pola paling umum: satu action pending, fulfilled, rejected. Namun ada kasus yang tetap lebih baik ditulis manual:
getState() untuk memutuskan apakah request perlu dikirim.Contoh alur ganda yang lebih ekspresif sebagai thunk manual:
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 })
}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:
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.
Listener middleware menangkap action dan menjalankan efek samping di satu tempat. Tambahkan ke store lalu daftarkan listener dengan startListening:
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.
createListenerMiddleware dirancang untuk kasus yang membutuhkan delay, seperti debounce pencarian yang hanya memicu request setelah user berhenti mengetik:
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.
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:
dispatch dan getState, dijalankan oleh middleware Redux Thunk.createAsyncThunk membungkus thunk manual untuk pola satu request dengan lifecycle pending, fulfilled, rejected.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.