STATE-MANAGEMENT FINITE-STATE-MACHINE XSTATE DESIGN-PATTERNS SOFTWARE-DESIGN FRONTEND BACKEND CLEAN-CODE COMPLEXITY REACT JAVASCRIPT

Mengelola Kompleksitas State dengan Finite State Machine: Panduan Praktis Menggunakan XState

⏱️ 20 menit baca
👨‍💻

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.

FSM memaksa kita untuk berpikir secara eksplisit tentang:

  1. Apa saja state yang mungkin? (e.g., Merah, Kuning, Hijau)
  2. Event apa yang bisa terjadi? (e.g., TIMER_EXPIRED)
  3. Bagaimana sistem bereaksi terhadap event di setiap state? (e.g., Jika di Merah dan TIMER_EXPIRED, pindah ke Hijau).

✅ Keuntungan Menggunakan FSM:

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:

💡 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:

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