Mengelola Kompleksitas State dengan Finite State Machine: Panduan Praktis Menggunakan XState
1. Pendahuluan
Pernahkah Anda merasa “state” aplikasi Anda seperti benang kusut yang sulit diurai? Logika kondisional yang bercabang (if/else bertumpuk), transisi state yang tidak terduga, dan bug yang muncul entah dari mana? Ini adalah masalah umum yang dihadapi banyak developer, terutama saat aplikasi tumbuh semakin kompleks.
Bayangkan sebuah aplikasi e-commerce. Proses checkout saja bisa memiliki banyak state: Idle, AddingItems, FillingShippingInfo, ValidatingPayment, ProcessingOrder, OrderSuccess, OrderFailed, CancellingOrder. Setiap state ini memiliki aturan dan transisi tersendiri. Jika dikelola secara ad-hoc, kode Anda akan cepat menjadi “spaghetti code” yang sulit dibaca, diuji, dan dipelihara.
Di sinilah Finite State Machine (FSM) masuk sebagai penyelamat. FSM adalah pola desain yang memungkinkan kita memodelkan perilaku suatu sistem dengan jelas, membatasi state yang mungkin, dan mendefinisikan transisi antar state berdasarkan event yang spesifik. Hasilnya? Kode yang lebih prediktif, mudah diuji, dan bebas bug.
Dalam artikel ini, kita akan menyelami dunia FSM dan bagaimana XState, sebuah library JavaScript/TypeScript yang powerful, dapat membantu Anda mengimplementasikan pola ini dengan elegan di aplikasi Anda. Baik Anda membangun UI interaktif di frontend atau mengelola alur kerja kompleks di backend, FSM dengan XState bisa menjadi alat andalan Anda. Mari kita mulai!
2. Apa Itu Finite State Machine (FSM)?
🎯 Finite State Machine (FSM) adalah model matematika komputasi yang digunakan untuk merancang sistem yang perilakunya dapat dipecah menjadi sejumlah state (keadaan) terbatas. Sistem berada dalam satu state pada satu waktu, dan dapat berpindah ke state lain hanya melalui transisi yang dipicu oleh event tertentu.
Mari kita analogikan dengan sesuatu yang familiar: Lampu Lalu Lintas.
- States (Keadaan):
MerahKuningHijau
- Events (Pemicu):
TIMER_EXPIRED(waktu habis)BUTTON_PRESSED(tombol penyeberangan ditekan) - untuk kasus yang lebih kompleks
- Transitions (Perpindahan):
- Dari
MerahkeHijau(setelahTIMER_EXPIRED) - Dari
HijaukeKuning(setelahTIMER_EXPIRED) - Dari
KuningkeMerah(setelahTIMER_EXPIRED)
- Dari
FSM memaksa kita untuk berpikir secara eksplisit tentang:
- Apa saja state yang mungkin? (e.g.,
Merah,Kuning,Hijau) - Event apa yang bisa terjadi? (e.g.,
TIMER_EXPIRED) - Bagaimana sistem bereaksi terhadap event di setiap state? (e.g., Jika di
MerahdanTIMER_EXPIRED, pindah keHijau).
✅ Keuntungan Menggunakan FSM:
- Prediktabilitas: Sistem hanya bisa berada di state yang valid dan transisi hanya terjadi jika event yang valid dipicu. Ini menghilangkan kemungkinan state yang “mustahil” atau tidak terduga.
- Visualisasi: FSM dapat dengan mudah digambarkan dalam diagram, sehingga memudahkan pemahaman alur kerja yang kompleks.
- Modularitas: Setiap state dan transisi memiliki tanggung jawabnya sendiri, membuat kode lebih terorganisir.
- Debugging Lebih Mudah: Ketika terjadi bug, Anda tahu persis di state mana sistem berada dan event apa yang memicu transisi yang salah.
- Testing Lebih Efektif: Anda dapat menulis test case untuk setiap state dan transisi, memastikan semua jalur berfungsi dengan benar.
3. Konsep Dasar XState
XState adalah library JavaScript/TypeScript yang mengimplementasikan konsep FSM (dan Statecharts, yang merupakan ekstensi dari FSM) dengan sangat baik. Mari kita lihat bagaimana kita mendefinisikan mesin FSM sederhana menggunakan XState.
Pertama, install XState:
npm install xstate
# atau
yarn add xstate
Sekarang, mari kita buat mesin lampu lalu lintas kita:
// trafficLightMachine.js
import { createMachine, interpret } from "xstate";
// 📌 1. Mendefinisikan Finite State Machine
const trafficLightMachine = createMachine({
id: "trafficLight", // ID unik untuk mesin ini
initial: "red", // State awal saat mesin dimulai
states: {
// Definisi semua state yang mungkin
red: {
on: {
// Event yang bisa dipicu saat di state 'red'
TIMER: "green", // Jika event TIMER terjadi, pindah ke state 'green'
},
},
green: {
on: {
TIMER: "yellow",
},
},
yellow: {
on: {
TIMER: "red",
},
},
},
});
// 📌 2. Menginterpretasikan dan Menjalankan Mesin
const trafficLightService = interpret(trafficLightMachine)
.onTransition((state) => {
console.log(`Lampu sekarang: ${state.value}`); // Log state saat transisi terjadi
})
.start(); // Mulai mesin, ini akan masuk ke state 'initial' ('red')
console.log(`Lampu awal: ${trafficLightService.state.value}`); // Output: Lampu awal: red
// 📌 3. Mengirim Event untuk Memicu Transisi
trafficLightService.send("TIMER"); // Output: Lampu sekarang: green
trafficLightService.send("TIMER"); // Output: Lampu sekarang: yellow
trafficLightService.send("TIMER"); // Output: Lampu sekarang: red
trafficLightService.send("TIMER"); // Output: Lampu sekarang: green
// trafficLightService.send('ACCIDENT'); // Event ini tidak akan melakukan apa-apa karena tidak didefinisikan
Dalam contoh di atas:
createMachineadalah fungsi utama untuk mendefinisikan mesin.idadalah pengenal unik untuk mesin Anda.initialmenentukan state awal.statesadalah objek yang berisi definisi setiap state.ondi dalam setiap state mendefinisikan transisi yang diizinkan saat mesin berada di state tersebut, dipicu oleh event tertentu.
💡 Tips: XState memiliki visualizer online yang sangat membantu! Anda bisa menyalin definisi mesin Anda ke Stately.ai Visualizer untuk melihat diagram FSM secara interaktif.
4. State yang Lebih Kompleks: Nested States & Parallel States
Sistem dunia nyata jarang sesederhana lampu lalu lintas. XState (berdasarkan Statecharts) memungkinkan kita menangani kompleksitas ini dengan:
4.1. Nested States (Hierarchical States)
Nested states memungkinkan Anda memiliki state di dalam state lain. Ini sangat berguna untuk mengelompokkan perilaku terkait dan mengelola state yang lebih rinci.
Bayangkan proses pengiriman form:
idlesubmitting(yang di dalamnya bisavalidating,sendingRequest,waitingForResponse)successerror
import { createMachine } from "xstate";
const formMachine = createMachine({
id: "form",
initial: "idle",
states: {
idle: {
on: {
SUBMIT: "submitting",
},
},
submitting: {
initial: "validating", // State awal saat masuk ke 'submitting'
states: {
validating: {
on: {
SUCCESS: "sendingRequest",
ERROR: "idle", // Kembali ke idle jika validasi gagal
},
},
sendingRequest: {
on: {
SUCCESS: "waitingForResponse",
ERROR: "idle",
},
},
waitingForResponse: {
on: {
SUCCESS: "#form.success", // Transisi ke state 'success' di level root
ERROR: "#form.error", // Transisi ke state 'error' di level root
},
},
},
},
success: {
type: "final", // Menandai ini sebagai state akhir
},
error: {
on: {
RETRY: "submitting",
},
},
},
});
// Gunakan visualizer untuk melihat bagaimana state 'submitting' memiliki sub-state!
Perhatikan initial: 'validating' di dalam submitting dan bagaimana kita bisa melompat keluar dari nested state menggunakan #form.success.
4.2. Parallel States
Parallel states memungkinkan beberapa mesin FSM kecil berjalan secara bersamaan dan independen dalam satu mesin induk. Ini berguna ketika Anda memiliki bagian-bagian aplikasi yang bisa beroperasi secara mandiri.
Contoh: Aplikasi musik (memutar musik) dan notifikasi (menampilkan/menyembunyikan). Keduanya bisa berjalan paralel.
import { createMachine } from "xstate";
const userDashboardMachine = createMachine({
id: "userDashboard",
type: "parallel", // Ini adalah mesin paralel
states: {
authentication: {
initial: "loggedIn",
states: {
loggedIn: {
on: { LOGOUT: "loggedOut" },
},
loggedOut: {
on: { LOGIN: "loggedIn" },
},
},
},
notifications: {
initial: "enabled",
states: {
enabled: {
on: { DISABLE_NOTIFICATIONS: "disabled" },
},
disabled: {
on: { ENABLE_NOTIFICATIONS: "enabled" },
},
},
},
},
});
// Saat diinterpretasikan, mesin ini akan berada di state { authentication: 'loggedIn', notifications: 'enabled' }
// Anda bisa mengubah salah satu tanpa memengaruhi yang lain.
Dengan type: 'parallel', mesin userDashboard akan secara bersamaan berada di state authentication.loggedIn DAN notifications.enabled.
5. Efek Samping dan Aksi: actions dan invoke
FSM tidak hanya tentang perubahan state. Seringkali, saat state berubah atau event dipicu, kita perlu melakukan sesuatu: memanggil API, memperbarui UI, logging, dll. Ini disebut efek samping (side effects) atau aksi (actions).
5.1. actions
actions adalah efek samping sinkron yang terjadi saat masuk ke suatu state (entry), keluar dari state (exit), atau saat transisi tertentu.
import { createMachine, assign } from "xstate";
const bookingMachine = createMachine(
{
id: "booking",
initial: "idle",
context: {
// Data yang bisa disimpan dan dimodifikasi oleh mesin
seats: 0,
errorMessage: undefined,
},
states: {
idle: {
on: {
START_BOOKING: {
target: "selectingSeats",
actions: "logBookingStarted", // Panggil aksi saat transisi
},
},
},
selectingSeats: {
on: {
ADD_SEAT: {
actions: assign({
// assign adalah action XState untuk mengubah context
seats: (context, event) => context.seats + 1,
}),
},
REMOVE_SEAT: {
actions: assign({
seats: (context, event) => Math.max(0, context.seats - 1),
}),
},
CONFIRM_SEATS: {
target: "processingPayment",
cond: (context) => context.seats > 0, // Hanya transisi jika kursi > 0
},
},
},
processingPayment: {
entry: "showLoadingSpinner", // Aksi saat masuk state ini
exit: "hideLoadingSpinner", // Aksi saat keluar state ini
on: {
PAYMENT_SUCCESS: "bookingComplete",
PAYMENT_FAILED: {
target: "selectingSeats", // Kembali ke memilih kursi
actions: assign({ errorMessage: "Pembayaran gagal, coba lagi!" }),
},
},
},
bookingComplete: {
type: "final",
entry: "sendConfirmationEmail",
},
},
},
{
actions: {
// Implementasi aksi di sini
logBookingStarted: (context, event) => {
console.log("Booking dimulai!");
},
showLoadingSpinner: () => {
console.log("Menampilkan spinner loading...");
},
hideLoadingSpinner: () => {
console.log("Menyembunyikan spinner loading...");
},
sendConfirmationEmail: (context) => {
console.log(`Email konfirmasi dikirim untuk ${context.seats} kursi.`);
},
},
},
);
assign adalah action bawaan XState yang sangat berguna untuk memodifikasi context (data internal) mesin.
5.2. invoke (Efek Samping Asynchronous)
Untuk efek samping yang berjalan lama atau asinkron (misalnya, memanggil API, timer, subscribe ke WebSocket), kita menggunakan invoke. invoke memungkinkan Anda “memanggil” sebuah layanan (promise, callback, observable) saat masuk ke suatu state.
import { createMachine, assign, interpret } from "xstate";
const userProfileMachine = createMachine({
id: "userProfile",
initial: "idle",
context: {
user: null,
error: undefined,
},
states: {
idle: {
on: {
FETCH_PROFILE: "loading",
},
},
loading: {
invoke: {
// 💡 Panggil layanan saat masuk ke state 'loading'
id: "fetchUserProfile", // ID untuk layanan ini
src: (context, event) =>
fetch(`/api/users/${event.userId}`) // Asumsi event.userId ada
.then((res) => res.json()),
onDone: {
// Apa yang terjadi jika promise berhasil
target: "loaded",
actions: assign({
user: (context, event) => event.data, // event.data berisi hasil dari promise
}),
},
onError: {
// Apa yang terjadi jika promise gagal
target: "error",
actions: assign({
error: (context, event) =>
event.data.message || "Gagal memuat profil.",
}),
},
},
},
loaded: {
on: {
EDIT_PROFILE: "editing",
REFRESH: "loading",
},
},
editing: {
// ... logika untuk mengedit profil
},
error: {
on: {
RETRY: "loading",
},
},
},
});
const service = interpret(userProfileMachine)
.onTransition((state) => console.log(state.value, state.context))
.start();
service.send({ type: "FETCH_PROFILE", userId: "123" });
// Output akan menunjukkan transisi dari 'idle' -> 'loading', lalu 'loaded' atau 'error'
invoke adalah salah satu fitur paling powerful dari XState karena memungkinkan kita mengelola alur asinkron yang kompleks dengan cara yang terstruktur.
6. Mengintegrasikan XState ke Aplikasi Anda (Contoh React)
XState bersifat framework-agnostic, artinya bisa digunakan dengan React, Vue, Angular, atau bahkan Node.js. Untuk React, ada @xstate/react yang menyediakan hook useMachine untuk integrasi yang mulus.
npm install @xstate/react
# atau
yarn add @xstate/react
// UserProfile.jsx
import React from "react";
import { useMachine } from "@xstate/react";
import { createMachine, assign } from "xstate";
// Definisi mesin yang sama seperti di bagian 5.2
const userProfileMachine = createMachine({
id: "userProfile",
initial: "idle",
context: {
user: null,
error: undefined,
},
states: {
idle: {
on: {
FETCH_PROFILE: "loading",
},
},
loading: {
invoke: {
id: "fetchUserProfile",
src: (context, event) =>
new Promise((resolve, reject) => {
// Simulasi panggilan API
setTimeout(() => {
if (event.userId === "123") {
resolve({ id: "123", name: "Budi", email: "budi@example.com" });
} else {
reject({ message: "User tidak ditemukan" });
}
}, 1500);
}),
onDone: {
target: "loaded",
actions: assign({ user: (context, event) => event.data }),
},
onError: {
target: "error",
actions: assign({ error: (context, event) => event.data.message }),
},
},
},
loaded: {
on: {
EDIT_PROFILE: "editing",
REFRESH: "loading",
},
},
editing: {
// ...
},
error: {
on: {
RETRY: "loading",
},
},
},
});
function UserProfile({ userId }) {
// 🎯 useMachine hook mengembalikan state saat ini dan fungsi send untuk mengirim event
const [current, send] = useMachine(userProfileMachine);
// useEffect untuk memicu event awal
React.useEffect(() => {
send({ type: "FETCH_PROFILE", userId });
}, [userId, send]);
return (
<div
style={{
padding: "20px",
border: "1px solid #ccc",
borderRadius: "8px",
maxWidth: "400px",
margin: "20px auto",
}}
>
<h2>Profil Pengguna</h2>
{current.matches("idle") && <p>Siap memuat profil...</p>}
{current.matches("loading") && <p>⏳ Memuat profil...</p>}
{current.matches("loaded") && (
<div>
<p>
<strong>ID:</strong> {current.context.user?.id}
</p>
<p>
<strong>Nama:</strong> {current.context.user?.name}
</p>
<p>
<strong>Email:</strong> {current.context.user?.email}
</p>
<button onClick={() => send("EDIT_PROFILE")}>Edit Profil</button>
<button onClick={() => send("REFRESH")}>Refresh</button>
</div>
)}
{current.matches("error") && (
<div style={{ color: "red" }}>
<p>❌ Error: {current.context.error}</p>
<button onClick={() => send("RETRY")}>Coba Lagi</button>
</div>
)}
{current.matches("editing") && (
<div>
<p>Anda sedang dalam mode edit...</p>
{/* Form edit di sini */}
</div>
)}
</div>
);
}
export default UserProfile;
Dengan useMachine, Anda mendapatkan current (objek state saat ini, termasuk value dan context) dan send (fungsi untuk mengirim event ke mesin). Ini membuat logika UI Anda sangat deklaratif dan terhubung langsung dengan definisi FSM Anda.
Kesimpulan
Mengelola state adalah salah satu tantangan terbesar dalam pengembangan aplikasi, dan seringkali menjadi sumber utama bug dan kompleksitas. Dengan Finite State Machine dan library seperti XState, kita memiliki alat yang ampuh untuk mengubah kekacauan menjadi keteraturan.
Kita telah melihat bagaimana FSM membantu kita mendefinisikan state, event, dan transisi secara eksplisit. XState membawa konsep ini lebih jauh dengan nested states, parallel states, actions, dan fitur invoke untuk menangani efek samping asinkron. Dengan mengadopsi pola ini, Anda tidak hanya menulis kode yang lebih bersih dan mudah diuji, tetapi juga membangun sistem yang lebih prediktif dan tangguh.
Jangan ragu untuk mulai bereksperimen dengan XState di proyek Anda berikutnya. Mungkin di awal terasa ada kurva pembelajaran, tetapi manfaat jangka panjangnya dalam mengelola kompleksitas akan sangat sepadan. Selamat mencoba!
🔗 Baca Juga
- Memahami Pola Desain Perangkat Lunak: Fondasi Kode yang Bersih dan Fleksibel
- Modern Frontend State Management: Memilih dan Mengelola State di Aplikasi Web Skala Besar
- Menggali Lebih Dalam React Hooks: Panduan Praktis untuk Developer Modern
- Idempotency dalam Sistem Terdistribusi: Membangun Aplikasi yang Aman dan Konsisten