Membuat proses Sign in dengan React dan State Machine

20 Sep 2020 ยท 13 menit baca

Link berhasil di copy

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.

Initial display

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

Table

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.

Installing

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 parameter evt.
  • Selanjutnya, apabila request berhasil, ditandai dengan input atau trigger "done", maka state akan berpindah ke success dan mesin akan mengganti value dari properti result pada context, 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

Demo

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

2. Video tentang FSM di youtube

3. Dokumentasi Robot

Emot's Space ยฉ 2024