@ wordpress/dataとTypeScriptを備えた有限ステートマシン
公開: 2021-09-16アプリケーションでログイン画面を設計するとします。 ユーザーが初めてそれを見るとき、空のフォームがあります。 ユーザーはフィールドに入力し、すべての設定が完了したら、ログインボタンを押して資格情報を検証し、ログインします。検証が成功すると、次の画面に移動します。 そうでない場合は、エラーメッセージが表示され、再試行するように求められます。
このような画面の状態を実装し、TypeScriptと@wordpress/data
を使用してアプリケーションの状態を定義する方法に関する私の投稿を読んだ場合、次のようなことを行うと思います。
type State = { readonly username: string; readonly password: string; readonly isLoggingIn: boolean; readonly errorMessage: string; };
状態を管理するために必要なすべてのフィールドがありますが…これは私たちができる最善のことですか?
有限状態機械
有限状態マシン(FSM)は、大まかに言えば、状態の有限集合とそれらの間の可能な遷移を定義できる数学モデルです。 (ウィキペディアの詳細情報)。 この抽象化は、アプリケーションの状態のモデリングに関して私たちのニーズに完全に適合するため、私はこの抽象化が好きです。 たとえば、ログイン画面の状態図は次のようになります。

ご覧のとおり、これは、実装したい動作を明確かつ簡潔にキャプチャします。
@wordpress/data
で状態を定義する
Reactの紹介シリーズでは、 @wordpress/data
パッケージを使用してアプリの状態を作成および管理する方法を説明しました。 これは、データストアの主要コンポーネントを定義することで実現しました。
- 状態を照会するためのセレクターのセット
- 更新要求をトリガーする一連のアクション
- 現在の状態と更新アクションが与えられると、状態を更新するためのレデューサー関数
本質的に、 @wordpress/data
のストアは、アクションを使用してレデューサー関数がある状態から別の状態に「遷移」するため、すでに有限状態マシンのように動作します。
( state: State, action: Action ) => State
@wordpress/data
でTypeScriptを使用して有限ステートマシンを定義する方法
@wordpress/data
ストアは、上記の数行で紹介したFSMモデルにかなり近いようです。特定の状態とそれらの間の遷移が欠落しているだけです。では、どのようにできるかを詳しく見ていきましょう。これらの問題を一度に1ステップずつ修正します。
明示的な状態
この投稿は、アプリの状態の実装の可能性を示すことから始めました。 私たちの最初の提案は、仕事を成し遂げた一連の属性でしたが、それは有限状態マシンのようにはまったく見えませんでした。 それでは、モデルで状態が明示的に定義されていることを確認することから始めましょう。
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; };
かなり明白ですね状態ごとに1つのタイプがあり、アプリの全体的な状態は、タイプの識別された結合として定義されます。 これは、(a)状態が「さまざまなタイプの結合」(つまり、 Form
、 LoggingIn
、またはその他)であり、(b)属性(この場合は)があるため、このように呼ばれます。 、 status
)。これにより、各瞬間の特定の状態を識別できます。
このソリューションは、最初に持っていたものよりもはるかに優れていると思います。 元のソリューションでは、ログイン( isLoggingIn
をtrue
に設定するだけ)と同時にエラー状態( errorMessage
を空以外の値に設定するだけ)になる可能性があるため、「無効な状態」を定義できました。ストリング)。 しかし、これは明らかに意味がありません! どちらですか? ログインしていますか、それともエラーが表示されますか?
一方、新しいソリューションは、状態を表す場合にはるかに正確です。つまり、「無効な」状態を「不可能」にします。 LoggingIn
を使用している場合、エラーメッセージを設定する方法はありません(属性はありません!)。 Error
が表示されている場合は、ログインできません。もっといいですね。
行動
@wordpress/data
では、アクションによってストアが更新されます。 状態をFSMとしてモデル化しているため、アクションは元の図の矢印になります。 あなたがしなければならないのは、それらをタイプの識別された結合としてモデル化することです(慣例により、ディスクリミネーターは通常type
と呼ばれます)そしてあなたは行ってもいいです:
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'; };
私たちの行動は、元の図にあった矢印を表すことになっていますよね? さて、あなたのことはわかりませんが、これらのアクションタイプは矢印のようには見えないと思います! 矢印には方向があります(ある状態から別の状態に移動します)。 アクションはしません。

特定のアクションは、ある状態から別の状態に移行する場合にのみ役立つことをコードで明確にする方法が必要です。 そしてこれまでのところ、私が見つけた最善の解決策は、レデューサー自体を使用することです。
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; }
レデューサーがstate
のstatus
をフィルタリングすることから始める場合、矢印の「ソース」がわかります。 次に、現在のアクションを確認し、それが「外向きの矢印」である場合は、新しい「ターゲット」状態を生成します。
上記のスニペットで、これがどのように機能するかを明確に確認できます。 現在のstatus
がform
の場合、 SET_CREDENTIALS
アクションは、私たちがいたのと同じ状態になり(資格情報を更新します)、 LOGIN
アクションは状態をlogging-in
変更します。 この状態の他のアクションは無視されるため、状態は変更されません。
それはすべて良いことで完璧ですが…わかりません。コードはそれほど好きではありませんね。 コードが明確で、簡潔で、わかりやすく、タイプセーフであることを確認したいと思います。 できますか?
強く型付けされたレデューサー
発生したばかりの混乱を修正するには、FSMで可能な各状態に独自のレデューサーが含まれるように、 reducer
をリファクタリングする必要があります。 これを正しく行うと、強く型付けされ、何が許可され、何が許可されないかが明確になります。
まず、アプリケーションで状態ごとに型を作成することから始めましょう。 各タイプは、特定の状態から別の場所に移動できるアクションをグループ化します。 たとえば、 Form
の状態には2つの外向きの矢印( SetCredentials
とLogin
)があるので、2つのアクションを結合してFormAction
タイプを作成しましょう。 状態ごとにこのプロセスを繰り返すと、次のようになります。
type FormAction = SetCredentials | Login; type LoggingInAction = ShowError | ShowApp; type SuccessAction = ...; type ErrorAction = BackToLoginForm; type Action = | FormAction | LoggingInAction | SuccessAction | ErrorAction;
次に、必要なすべてのレデューサーを定義します。 繰り返しますが、状態ごとに1つです。
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 ) => ...
TypeScriptと定義したタイプのおかげで、ソースの状態と各レデューサーが期待する一連のアクション、および結果として取得できるターゲットの状態を非常に正確に把握できるようになりました。
最後に、すべてを独自のreducer
関数と結び付ける必要があります。
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; } }
結果として得られるコードは、私の意見では、より明確で保守が容易です。 さらに、TypeScriptは、私たちが行っていることを検証し、欠落しているシナリオをキャッチできるようになりました。 素晴らしい!
結論
この投稿では、有限状態マシンとは何か、そしてTypeScriptを使用して@wordpress/data
ストアに有限状態マシンを実装する方法を見てきました。 最終的なソリューションは非常にシンプルですが、多くの利点があります。アプリの状態と意図をより適切にキャプチャし、TypeScriptの機能を活用します。
この投稿が気に入っていただけたら幸いです。 もしそうなら、それをあなたの友人や同僚と共有してください。 そして、下のコメントセクションでこれらの問題にどのように取り組んでいるか教えてください。
UnsplashのPatrickHendryによる注目の画像。