はじめに
Reactでは useState や useReducer を用いた状態管理が一般的です。
しかし、画面が複雑になるにつれて、
- 状態とデータが混在する
- 親子コンポーネント間の依存が増える
- 「どこで状態が変わったのか」が分かりにくい
と感じることがあります。
そこで今回は、
「状態(State)」と「データ(Data)」を明確に分離する
というルールで、状態遷移を中心にしたアーキテクチャを試作しました。
考え方の着想には数学の圏論の影響がありますが、本記事では圏論そのものは扱わず、状態遷移を中心とした設計手法として紹介します。
サンプルとして、シンプルなログイン画面を実装しています。
コンセプト
この設計では、次の3つを基本ルールとしています。
- Stateは意味だけを持つ
- DataはRedux Storeに保存する
- 画面はStateのみを見て描画する
例えば Input コンポーネントであれば、
State
------
beforeInit
empty
entering
entered
入力値
"sample@example.com"
は State ではなく Data として扱います。
システム構成
React View
├── Screen
├── Login
├── Main
└── TestInput
Redux Store
└── common.data
Function Registry
└── funcStore
役割は次のように分かれています。
| 要素 | 役割 |
|---|---|
| Screen | 画面遷移 |
| Login | ログイン画面全体 |
| TestInput | 入力欄の状態管理 |
| Redux | 入力データ保持 |
| funcStore | イベント(射)の登録 |
状態遷移
Screen
beforeInit
│
▼
login
│
▼
main
│
▼
logout
Login
beforeInit
│
▼
noComplete
│
▼
complete
TestInput
beforeInit
│
▼
empty
│
▼
entering
│
▼
entered
画面の表示はこれらの状態だけを見て切り替えます。
DataはReduxへ
入力値はuseStateでは持ちません。
例えば
{
"login": {
"id": "sample@example.com",
"password": "password"
}
}
のように Redux Store に保存します。
UIは
State
Data
を完全に分離して考えます。
コンポーネント間通信
親子間では次の3つだけを受け渡します。
pState
pRef
mappingKey
pState
親の状態を子へ通知します。
Login
↓
TestInput
のように利用します。
pRef
子の状態を親へ通知します。
TestInput
↓
Login
親は子の状態だけを監視し、
- ログインボタンを有効化する
- 入力完了を判定する
などを行います。
mappingKey
入力項目等の複数あるコンポーネントの差別化として使用します。
コンポーネント全体で一意になるように指定します。
射(Function)を公開する
状態遷移は直接呼び出さず、関数(本記事では圏論にならって「射」と呼びます)として公開します。
例えば
register("login", login)
register("logout", logout)
必要な場所では
call("login",[])
のように実行します。
状態遷移そのものを公開するイメージです。
この設計のメリット
状態が見やすい
Stateが意味だけを持つため、
beforeInit
empty
entering
entered
の流れが明確になります。
DataとUIが分離できる
入力値はRedux、
画面表示はState。
責務が混ざりません。
Inputの再利用が容易
Inputは
- 状態管理
- 入力管理
だけを担当します。
どの画面でも同じInputコンポーネントを利用できます。
今後改善したいこと
現在は学習用の実験実装なので、改善点も多くあります。
- enumによる状態管理
- 状態機械の共通化
- 型安全なAction管理
- funcStoreの改善
- Redux依存の整理
などを検討しています。
まとめ
このアプリはログイン画面を作ることが目的ではありません。
Reactを
- State(状態)
- Data(データ)
- Action(射)
に分離するとどのような設計になるかを試すための実験です。
まだ改善点は多くありますが、今後も状態機械をベースにしたフロントエンド設計を試していきたいと思います。