Mesin Status Terbatas dengan @wordpress/data dan TypeScript

Diterbitkan: 2021-09-16

Misalkan kita ingin mendesain layar login di aplikasi kita. Saat pengguna melihatnya untuk pertama kali, ada formulir kosong. Pengguna mengisi kolom dan, setelah semuanya siap, mereka menekan tombol masuk untuk memvalidasi kredensial dan, yah, masuk. Jika validasi berhasil, mereka pindah ke layar berikutnya. Tetapi jika tidak, mereka disajikan pesan kesalahan dan diminta untuk mencoba lagi.

Jika Anda menerapkan status layar seperti itu dan Anda membaca posting saya tentang cara mendefinisikan status aplikasi dengan TypeScript dan @wordpress/data , saya yakin Anda akan melakukan sesuatu seperti ini:

 type State = { readonly username: string; readonly password: string; readonly isLoggingIn: boolean; readonly errorMessage: string; };

yang memiliki semua bidang yang diperlukan untuk mengelola negara, tetapi… apakah ini yang terbaik yang bisa kita lakukan?

Mesin Negara Terbatas

Sebuah mesin negara yang terbatas (FSM), secara umum, model matematika yang memungkinkan kita untuk mendefinisikan satu set terbatas negara dan kemungkinan transisi antara mereka. (informasi lebih lanjut di Wikipedia). Saya menyukai abstraksi ini karena sangat sesuai dengan kebutuhan kita dalam hal memodelkan keadaan aplikasi kita. Misalnya, diagram status untuk layar masuk kita mungkin terlihat seperti ini:

Diagram status formulir login
Diagram status formulir login.

yang, seperti yang Anda lihat, menangkap dengan jelas dan ringkas perilaku yang ingin kita terapkan.

Mendefinisikan Negara dengan @wordpress/data

Dalam seri pengantar React kami, kami melihat cara menggunakan paket @wordpress/data untuk membuat dan mengelola status aplikasi kami. Kami melakukannya dengan mendefinisikan komponen utama penyimpanan data kami:

  • Satu set penyeleksi untuk menanyakan status
  • Serangkaian tindakan untuk memicu permintaan pembaruan
  • Fungsi peredam untuk, mengingat status saat ini dan tindakan pembaruan, memperbarui status

Intinya, penyimpanan di @wordpress/data sudah berperilaku seperti mesin keadaan terbatas, karena fungsi peredam "bertransisi" dari satu keadaan ke keadaan lain menggunakan tindakan:

 ( state: State, action: Action ) => State

Cara Mendefinisikan Finite State Machine dengan TypeScript di @wordpress/data

Sepertinya @wordpress/data cukup dekat dengan model FSM yang kami perkenalkan beberapa baris di atas: kami hanya kehilangan status tertentu yang dapat kami masuki dan transisi di antara mereka… Jadi mari kita lihat lebih dekat bagaimana kita bisa perbaiki masalah ini, selangkah demi selangkah.

Negara Eksplisit

Kami memulai posting ini dengan menunjukkan kemungkinan implementasi status aplikasi kami. Proposal pertama kami adalah sekumpulan atribut yang menyelesaikan pekerjaan, tetapi itu tidak terlihat seperti mesin keadaan terbatas sama sekali. Jadi mari kita mulai dengan memastikan status kita didefinisikan secara eksplisit dalam model kita:

 type State = | Form | LoggingIn | Success | Error; type Form = { readonly status: 'form'; readonly username: string; readonly password: string; }; type LoggingIn = { readonly status: 'logging-in'; readonly username: string; readonly password: string; }; type Success = { readonly status: 'success'; // ... }; type Error = { readonly status: 'error'; readonly message: string; };

Cukup jelas, ya? Ada satu jenis per status dan status keseluruhan aplikasi didefinisikan sebagai gabungan tipe yang didiskriminasi. Disebut demikian karena (a) state adalah "penyatuan berbagai tipe" (yaitu, baik Form , atau LoggingIn , atau apa pun ) dan (b) kita memiliki atribut (dalam hal ini , status ) yang memungkinkan kita membedakan status spesifik apa yang kita miliki setiap saat.

Saya pikir solusi ini jauh lebih baik daripada yang kami miliki di awal. Dalam solusi asli kami, kami dapat mendefinisikan "status tidak valid," karena seseorang dapat masuk (cukup setel isLoggingIn ke true ) dan, pada saat yang sama, berada dalam status kesalahan (cukup setel errorMessage ke nilai selain yang kosong rangkaian). Tapi ini jelas tidak masuk akal! Yang mana? Apakah kita masuk atau kita seharusnya menunjukkan kesalahan?

Solusi baru, di sisi lain, jauh lebih tepat ketika mewakili keadaan: itu membuat keadaan "tidak valid" menjadi "mustahil." Jika kita berada di LoggingIn , tidak ada cara untuk mengatur pesan kesalahan (tidak ada atribut untuk itu!). Jika kami menampilkan Error , Anda tidak dapat login. Jauh lebih baik, bukan?

tindakan

Di @wordpress/data , tindakan memperbarui toko di negara kita. Karena kita memodelkan keadaan kita sebagai FSM, tindakan kita akan menjadi panah dari diagram asli kita. Yang harus Anda lakukan adalah memodelkannya sebagai penyatuan tipe yang didiskriminasi (berdasarkan konvensi, pembeda biasanya bernama type ) dan Anda siap melakukannya:

 type SetCredentials = { readonly type: 'SET_CREDENTIALS'; readonly username: string; readonly password: string; }; type Login = { readonly type: 'LOGIN'; }; type ShowApp = { readonly type: 'SHOW_APP'; // ... }; type ShowError = { readonly type: 'SHOW_ERROR'; readonly message: string; }; type BackToLoginForm = { readonly type: 'BACK_TO_LOGIN_FORM'; };

Tindakan kita seharusnya mewakili panah yang kita miliki di diagram asli kita, bukan? Yah, saya tidak tahu tentang Anda, tetapi saya pikir jenis tindakan ini tidak terlihat seperti panah sama sekali! Panah memiliki arah (mereka berpindah dari satu keadaan ke keadaan lain); tindakan tidak.

Kami membutuhkan cara untuk memperjelas dalam kode kami bahwa tindakan tertentu hanya berguna untuk berpindah dari satu keadaan ke keadaan lain. Dan sejauh ini solusi terbaik yang saya temukan adalah menggunakan peredam itu sendiri:

 function reducer( state: State, action: Action ): State { switch ( state.status ) { case 'form': switch ( action.type ) { case 'SET_CREDENTIALS': return { status: 'ready', username: action.username, password: action.password, }; case 'LOGIN': return { ...state, status: 'logging-in' }; } case ...: ... } return state; }

Jika peredam kami mulai dengan memfilter state status kami, kami tahu "sumber" panah kami. Kemudian, kami melihat tindakan saat ini dan, jika itu adalah "panah luar", kami menghasilkan status "target" baru.

Anda dapat dengan jelas melihat cara kerjanya dalam cuplikan di atas. Jika status saat ini adalah form , tindakan SET_CREDENTIALS membawa kita ke status yang sama seperti sebelumnya (memperbarui kredensial, ya) dan tindakan LOGIN mengubah status menjadi logging-in . Setiap tindakan lain dalam keadaan ini diabaikan dan, oleh karena itu, keadaan tidak berubah.

Itu semua bagus dan sempurna tapi… entahlah, aku tidak terlalu suka kodenya, ya? Saya ingin memastikan kode saya jelas, ringkas, cukup jelas, dan aman untuk tipe. Bisakah kita melakukan itu?

Peredam yang Diketik dengan Kuat

Untuk memperbaiki kekacauan yang baru saja kita alami, kita hanya perlu memperbaiki reducer kita sehingga setiap kemungkinan keadaan di FSM kita memiliki peredamnya sendiri. Jika kita melakukan ini dengan benar, mereka akan diketik dengan kuat dan akan jelas apa yang diizinkan dan apa yang tidak.

Mari kita mulai dengan membuat tipe untuk setiap status di aplikasi kita. Setiap jenis akan mengelompokkan tindakan yang dapat membawa kita dari keadaan tertentu ke tempat lain. Misalnya, status Form kita memiliki dua panah keluar ( SetCredentials dan Login ), jadi mari buat tipe FormAction dengan gabungan kedua tindakan. Ulangi proses untuk setiap negara bagian dan Anda akan mendapatkan ini:

 type FormAction = SetCredentials | Login; type LoggingInAction = ShowError | ShowApp; type SuccessAction = ...; type ErrorAction = BackToLoginForm; type Action = | FormAction | LoggingInAction | SuccessAction | ErrorAction;

Selanjutnya, tentukan semua reduksi yang kita butuhkan. Sekali lagi, satu untuk setiap negara bagian:

 function reduceForm( state: Form, action: FormAction ): Form | LoggingIn { switch ( action.type ) { switch ( action.type ) { case 'SET_CREDENTIALS': return { status: 'ready', username: action.username, password: action.password, }; case 'LOGIN': return { ...state, status: 'logging-in' }; } } reduceLoggingIn: ( state: LoggingIn, action: LoggingInAction ) => Success | Error reduceError: ( state: Error, action: ErrorAction ) => Form reduceSuccess: ( state: Success, action: SuccessAction ) => ...

Berkat TypeScript dan tipe yang baru saja kita definisikan, sekarang kita bisa sangat akurat tentang status sumber dan rangkaian tindakan yang diharapkan setiap peredam, serta status target yang bisa kita dapatkan sebagai hasilnya.

Akhirnya, kita hanya perlu mengikat semuanya dengan fungsi reducer yang unik:

 function reducer( state: State, action: Action ): State { switch ( state.status ) { case 'form': return reduceForm( state, action as FormActions ) ?? state; case 'logging-in': return reduceLoggingIn( state, action as LoggingInActions ) ?? state; case 'error': return reduceError( state, action as ErrorActions ) ?? state; case 'success': return reduceSuccess( state, action as SuccessActions ) ?? state; } }

Kode yang dihasilkan, menurut saya, lebih jelas dan lebih mudah dirawat. Dan, selain itu, TypeScript sekarang dapat memverifikasi apa yang kami lakukan dan menangkap skenario yang hilang. Luar biasa!

Kesimpulan

Dalam posting ini kita telah melihat apa itu finite state machine dan bagaimana kita bisa mengimplementasikannya di @wordpress/data store dengan TypeScript. Solusi terakhir kami cukup sederhana, namun menawarkan banyak keuntungan: ini menangkap status dan maksud aplikasi kami dengan lebih baik, dan memanfaatkan kekuatan TypeScript.

Saya harap Anda menyukai posting ini! Jika Anda melakukannya, silakan bagikan dengan teman dan kolega Anda. Dan tolong beri tahu saya bagaimana Anda mengatasi masalah ini di bagian komentar di bawah.

Gambar unggulan oleh Patrick Hendry di Unsplash.