Beberapa waktu lalu, Saya baru membaca surel mingguan dari Cassidy Williams edisi #158 yang salah satu link artikelnya membahas tentang Finite State Machine (FSM).
FSM secara sederhana adalah suatu konsep pengaturan keadaan (state) dari sebuah mesin atau aplikasi, dimana hanya boleh ada satu keadaan yang ada di satu waktu, yang berguna untuk mengatur operasi apa yang boleh terjadi atau feedback apa yang akan diberikan oleh mesin berdasarkan input tertentu.
Contoh FSM (atau kadang disebut juga Finite Automata) yang sering kita jumpai implementasinya adalah lampu lalu lintas. Lampu lalu lintas memiliki tiga keadaan yaitu Merah , Kuning dan Hijau, yang akan berubah berdasarkan waktu.
Contoh lain adalah vending machine atau mesin penjaja minuman otomatis, yang kira-kira memiliki lima keadaan yaitu Idle, Ready, Process, Finish, dan Error.
Pada keadaan Idle, mesin siap untuk menerima koin pengguna dan akan lanjut ke keadaan Ready saat koin dimasukkan. Selanjutnya, pengguna memilih minumanya pada keadaan Process, dan mesin akan mengolah pesanan. Jika gagal, misal harga minuman tidak sesuai dengan jumlah uang yang dimasukkan, mesin akan menuju keadaan Error, namun apabila berhasil maka mesin akan ke keadaan Finish lalu mengeluarkan minuman dan kembali ke keadaan Idle.
Gambar diatas merupakan diagram sederhana yang menggambarkan alur keadaan dari sebuah Vending Machine. Beberapa kata kunci yang perlu diperhatikan adalah:
- State, semua keadaan yang ada dalam mesin (Idle, Ready, Process, dll), disimbolkan dalam matematika dengan
Q
- Inputs, adalah masukkan yang menjadi pemicu terjadinya perubahan state (coin, drink choice, error feedback, dll), disimbolkan dengan
โ
- Initial State, adalah kondisi awal atau state yang pertama (Idle), disimbolkan
q0
- Final State, state yang terakhir (Finish), disimbolkan dengan
F
- Transition, semua perpindahan state yang terjadi akibat input tertentu dengan simbol
T
, lihat table berikut
Implementasi FSM di React
FSM pada dasarnya adalah model komputasi matematika yang bisa diterapkan di berbagai tempat, salah satunya dalam pemrograman Javascript. Ada dua library yang cukup terkenal untuk membuat FSM, yaitu XState dan Robot.
Kedua Library tersebut memiliki fungsi yang hampir sama. Perbedaan mereka lebih kepada syntax, ukuran modul, dan paradigma penggunaan.
Berikut adalah kode untuk membuat mesin toggle sederhana yang memiliki state aktif dan inaktif, menggunakan kedua library tersebut.
// Xstate const toggleMachine = Machine({ id: "toggle", initial: "inactive", states: { inactive: { on: { TOGGLE: "active", }, }, active: { on: { TOGGLE: "inactive", }, }, }, });
// Robot const toggleMachine = createMachine({ inactive: state(transition("toggle", "active")), active: state(transition("toggle", "inactive")), });
Jika diperhatikan, ada banyak kesamaan pada kedua kode diatas. Masing-masing memiliki State, Input, Transition, Initial State tapi tanpa Final state (Karena mesin ini untuk toggle, jadi ya bolak balik aja antara dua state).
Untuk menggunakan kedua mesin diatas di framework React, kita membutuhkan adapter khusus yang sudah disediakan oleh kedua library.
Sesuai judul artikel kali ini, Saya akan menggunakan library Robot untuk membuat FSM pada React. Robot memiliki adapater React-Robot untuk integrasi FSM tersebut dengan React. React-Robot menyediakan hooks untuk menjalankan mesin yang sudah dibuat pada komponen React.
import React from "react"; import ReactDOM from "react-dom"; import { createMachine, state, transition } from "robot3"; import { useMachine } from "react-robot"; const machine = createMachine({ off: state(transition("toggle", "on")), on: state(transition("toggle", "off")), }); function App() { const [current, send] = useMachine(machine); return ( <> <div>State: {current.name}</div> <button onClick={() => send("toggle")}>Toggle</button> </> ); } ReactDOM.render(<App />, document.querySelector("#app"));
Diatas adalah implementasi lengkap FSM pada komponen React, dengan fungsionalitas untuk beralih antar dua state, active dan inactive.
Hooks useMachine
menjalankan mesin yang sudah dibuat, dan mengembalikan dua buah variabel current
dan send
. current
berisi semua hal terkait state yang sekarang seperti nama dan data yang dia miliki. send
adalah sebuah fungsi untuk memberikan input yang akan merubah state mesin, yang pada kode diatas adalah string 'toggle'.
Pada setiap saat tombol Toggle diklik, mesin akan beralih dari keadaan aktif dan inaktif, jika dan hanya jika, input yang diberikan sesuai dengan kebutuhan. State tidak akan berganti jika misal kita mengganti input dengan string 'Yahoo'.
Selanjutnya kita akan coba membuat Halaman Sign in yang prosesnya akan menggunakan FSM untuk mengelola state dan http request ke API.
Membuat halaman Sign In dan integrasi dengan Robot
Untuk percobaan kali ini Saya akan menggunakan Codesandbox, tools sederhana yang memungkinkan kita untuk membuat mini-project menggunakan berbagai framework Javascript, termasuk React.
Pertama install semua library yang dibutuhkan. Klik pada tombol add dependency di sebelah kiri halaman, lalu akan muncul halaman seperti diatas. Cari dan install 3 modul berikut robot3, react-robot, terkahir, axios untuk melakukan http request.
Setelah semua terinstall, kita mulai dengan membuat form sign in sederhana pada file App.js
import React, { useState } from "react"; import "./styles.css"; export default function App() { const [credential, setCredential] = useState({ email: "", password: "" }); function handleChange(e) { const { name, value } = e.target; setCredential((current) => ({ ...current, [name]: value })); } function handleSubmit(e) { e.preventDefault(); } return ( <div className="App"> <h1>Sign in</h1> <form onSubmit={handleSubmit}> <input placeholder="email" value={credential.email} name="email" onChange={handleChange} /> <input placeholder="password" value={credential.password} type="password" name="password" onChange={handleChange} /> <button type="submit">Sign In</button> </form> </div> ); }
Selanjutnya kita membuat mesin yang akan menangani request ke API dan state pada form sebelumnya. State yang diperlukan untuk skenario sign in ini kira-kira empat macam: idle, loading, error, success. Dari nama-nama state tersebut sudah dapat dibayangkan apa yang terjadi pada masing-masingnya. Kita buat dulu mesinya dalam file machine.js, nanti Saya akan menjelaskanya dibawah
import { createMachine, state, transition, invoke, reduce } from "robot3"; const context = () => ({ result: "", error: "", }); export const machine = (request) => createMachine( { ready: state(transition("click", "loading")), loading: invoke( (ctx, evt) => request(evt.data), transition( "done", "success", reduce((ctx, evt) => ({ ...ctx, result: evt.data })) ), transition( "error", "error", reduce((ctx, evt) => ({ ...ctx, error: evt.error })) ) ), success: state(), error: state(transition("click", "loading")), }, context );
Di paling atas kode Saya mengimport fungsi yang dibutuhkan untuk merangkai mesin untuk sign in.
Selanjutnya Saya membuat fungsi bernama context
yang memiliki return value sebuah objek dengan property result
dan error
. Context ini nantinya jadi tempat menyimpan respon data maupun error dari http request.
Lanjut ke bagian inti yaitu FSM mari kita ulas dari awal:
- Pertama kita membuat mesin lewat fungsi
createMachine
dengan argumen sebuah objek berisi state dan operasi yang akan dijalankan. - Membuat state ready yang akan bertransisi ke state loading apabila input
"click"
diberikan - Membuat state loading. Didalamnya kita menggunakan fungsi
invoke
untuk menjalankan request yang akan dikirimkan ke server. Karena kita membuat sign in request, maka perlu mengirimkan data user credential yang didapatkan dari parameterevt
. - Selanjutnya, apabila request berhasil, ditandai dengan input atau trigger
"done"
, maka state akan berpindah ke success dan mesin akan mengganti value dari properti result padacontext
, dengan data yang didapat dari request. - Sebaliknya, saat request gagal, kita akan beralih ke state error, dan respon error dari API akan dimasukkan kedalam
context
. - Saat berada di state error, kita dapat melakukan request kembali, karena ada transisi untuk kembali ke state loading. Namun pada state success kita tidak akan bisa melakukan aktifitas apapun, karena tidak ada transisi disitu (dan memang kalau sign in berhasil harusnya dah pindah page kan, hehe)
Terakhir, kita menaruh context
pada argumen kedua dari createMachine
(atau kamu juga bisa langsung membuat anonymous function disini. Namun, agar kode lebih mudah dipahami, Saya membuatnya dalam variabel tersendiri).
Oke, kita sampai pada tahap paling mendebarkan untuk mengintegrasikan mesin kita dengan form yang sebelumnya telah dibuat. Mirip seperti contoh penggunaan mesin diawal, kita akan menggunakan hooks useMachine
untuk menjalankan mesin, berikut App.js terintegrasi
import React, { useState, useEffect } from "react"; import { useMachine } from "react-robot"; import axios from "axios"; import "./styles.css"; import { machine } from "./machine"; const signInApi = (data) => axios.post("https://reqres.in/api/login", data); const signInMachine = machine(signInApi); export default function App() { const [current, send] = useMachine(signInMachine); const { result, error } = current.context; const [credential, setCredential] = useState({ email: "", password: "" }); function handleChange(e) { const { name, value } = e.target; setCredential((current) => ({ ...current, [name]: value })); } function handleSubmit(e) { e.preventDefault(); send({ type: "click", data: credential }); } // to check success state useEffect(() => { if (current.name === "success") { console.log(result); } }, [current.name]); return ( <div className="App"> <h1>Sign in</h1> {current.name === "error" && <h3>{error.message}</h3>} {current.name === "success" && <h3>Sign In Success</h3>} <form onSubmit={handleSubmit}> {/* ... input field */} <button disabled={current.name === "loading"} type="submit"> Sign In </button> </form> </div> ); }
Dibagian paling atas kode yang sekarang, kita meng-import beberapa library yang dibutuhkan. Kemudian kita meng-import mesin yang telah dibuat sebelumnya dan menyiapkan endpoint API yang ingin di request.
Masuk kedalam komponen, kita menggunakan hooks useMachine
untuk menyalakan mesin, yang memberikan dua variabel, current
dan send
. Lalu kita melakukan sedikit destructuring pada variabel current
untuk mendapatkan result, hasil http request jika berhasil, dan error, jika request gagal.
Kemudian kita merubah sedikit fungsi handleSubmit
dan menambahkan input ke mesin lewat fungsi send
yang berisi tipe (input) dan data credential user yang akan sign in. Dibawahnya ada tambahan useEffect
sekedar untuk log respon dari server apabila sukses.
Masuk ke dalam form, kita membuat alert sederhana untuk menginformasikan apabaila request error atau sukses. Terakhir, pada button kita memberikan props disabled yang akan mencegah button berfungsi apabila state adalah loading.
Apabila semua kode diatas berjalan dengan baik, hasilnya adalah seperti ini
Saya menggunakan fake API dari reqres.in untuk percobaan ini. Credential pertama yang kumasukkan itu ngasal, untuk tes state error. Yang kedua adalah credential yang benar yang akan mendapatkan respon sukses dari server.
Dapat dilihat bahwa penggunaan mesin kita berhasil dengan baik. Button menjadi disabled ketika submit, pesan error muncul, success response juga di log dengan benar di console.
Mungkin untuk usecase sederhana seperti ini agak sulit merasakan manfaat penggunaan FSM. Namun jika nanti kamu harus berurusan dengan aplikasi yang, misal, memiliki enam atau lebih state, penggunaan FSM akan membuat state management-mu lebih baik.
Memang sedikit membingungkan ketika terbiasa menggunakan terma state di React yang merujuk suatu data yang tersimpan, dengan state pada FSM yang menunjukkan keadaan, sedangkan datanya disimpan dalam context.
Kode untuk percobaan diatas dapat kamu temukan disini. Setelah ini, Saya akan coba merubah mesin yang sebelumnya kita buat menjadi custom hooks dengan fungsionalitas sama, namun lebih mudah digunakan.
Mempermudah hidup dengan custom hooks
Kali ini misi kita adalah merubah mesin sign in menjadi custom hooks yang simple, bisa digunakan dengan case yang lain (register, reset password, etc), juga mengakses data dan fungsi lewat variabel yang lebih intuitif.
Rubahlah file machine.js menjadi seperti ini:
import { useMemo } from "react"; import { createMachine, state, transition, invoke, reduce } from "robot3"; import { useMachine } from "react-robot"; export function useRequest(request) { const context = () => ({ result: "", error: "" }); const machine = createMachine( { // ... machine } ); const requestMachine = useMemo(() => machine, []); const [current, send] = useMachine(requestMachine); const { result, error } = current.context; return { state: current.name, send, result, error, matches: (state) => current.name === state };
Dibagian paling atas kita menambahkan import useMemo
dari React, dan useMachine
dari React-Robot.
Lalu kita menamai ulang fungsi sebelumnya menjadi useRequest
agar menjadi hooks. Penamaanya bebas, namun Saya menggunakan nama tersebut karena nanti penggunaan hooks ini memang untuk melakukan http request ke API server.
Didalamnya, context
tetap sama, machine
juga sama, hanya saja kita perlu membungkusnya dengan useMemo
agar mesin tidak dijalankan ulang setiap komponen melakukan re-render.
Selanjutnya, useMachine
yang sebelum ini digunakan di App.js, kita pindahkan kesini untuk menjalankan mesin. Lalu melakukan destructuring seperti sebelumnya untuk mendapatkan state dan hasil request.
Terakhir, kita perlu mengembalikan state, data, dan fungsi yang ada, dalam bentuk objek, agar nanti kita bebas menggunakan data atau fungsi yang dibutuhkan. Selesai, mudah bukan? Sekarang kita tinggal menggunakanya di halaman sign in. Rubah App.js seperti berikut:
import React, { useEffect, useState } from "react"; import "./styles.css"; import axios from "axios"; import { useRequest } from "./machine"; const signInAPI = (data) => axios.post("https://reqres.in/api/login", data); export default function App() { const { state, send, result, error, matches } = useRequest(signInAPI); const [credential, setCredential] = useState({ email: "", password: "" }); // ... unchanged useEffect(() => { if (matches("success")) { // redirect to dashboard console.log(result); } }, [state]); return ( <div className="App"> <h1>Sign in</h1> {matches("error") && <h3>{error.message}</h3>} {matches("success") && <h3>Sign In Success</h3>} <form onSubmit={handleSubmit}> {/* ... input field */} <button disabled={matches("loading")} type="submit"> Sign In </button> </form> </div> ); }
Pada kode diatas, setelah meng-import hooks useRequest
, menggunakanya dengan endpoint sign in, dan melakukan destructure untuk hal yang dibutuhkan. Selanjutnya kita melakukan beberapa penyesuaian seperti, mengganti current.name
dengan state
di useEffect
. Mengganti comparison yang awalnya menggunakan ===
dengan fungsi matches()
.
Ketika kamu menjalankanya, hasilnya akan sama dengan yang sebelumnya, hanya kali ini lebih indah ๐. Lihat kode utuh untuk hooks disini.
Mungkin itu dulu untuk artikel kali ini. Saya yakin masih banyak kekurangan disana dan sini, dan kuharap hal itu tidak mengurangi tujuan awalku menulis, agar dapat mengetahui apa itu Finite State Machine dan implementasinya secara sederhana di React. Jika ada kritik dan saran bisa kamu sampaikan lewat email atau Twitter ku. Terimakasih.
Referensi
1. Deterministic Finite Automaton