Redux の State 設計の正規化の記事まとめ
この記事は、Redux の state 設計の正規化について調べた内容です。
背景として、自分の携わっているサービスにて、ContextAPI を使ってグローバルステートを管理していた。 が、サービスのグロースにてデータ構造的に無理が生じ始めたので、State 整理に伴って、これを機に Redux への切り替えを行おうと思った。
そのときに、調べたことをまとめる。
結論だが、「ContextAPI だと限界」というよりも、「State 設計を正しく行えていなかったので限界」が正解で、ContextAPI でも Redux でも、きちんと正規化してあげれば良かった。ということもあり、Redux を入れることは没になった。
公式ドキュメントを調べる
何はともあれ、公式のドキュメントを調べる。ドキュメントを呼んだ結果をだいたいまとめた。
公式 Doc: Normalizing State Shape · Redux
「データは正規化させる」ここで言う正規化は、RDB におけると正規化と同じ文脈。結局、フロント側で持っている State が大きくなって管理が大変なら、RDS と同じようなデータ構造を持ちましょう、ということだ。
「データ構造をネストさせない」JSON でデータを持つわけだが、やろうと思えば多段ネストが行える。が、便利さの裏返しで、管理がシンドくなるので、極力ネストが浅い構造にする。
「ID で参照」RelationaDatabase よろしくなデータ構造にする。後述で追記。
「リストで持たずにオブジェクトで保持する」これでアクセスのしやすさが向上する。具体的には、リストだと
list.find((x) => x.id === id);
のようにデータを見つけないといけないが、オブジェクトなら、
obj[id];
みたいにデータを見つけられる。ただ、データ群の順序情報が破棄されるので、/entites
でのみ、この形は使い、/domain
でリストで保持して順序を保存しておく。
「グルーピングする」Store の一番上位層を /entites
、/ui
、/domain
、で分割して、それぞれのデータ内容はそれぞれの配下に格納する。公式 Doc だと、文章がメインで具体例が少なすぎてわかりにくいが、「それぞれにどんな役割か?」は、後述の「スタートアップテック」さんのスライドがわかりやすかった。
正規化すると、データ構造がどうなるか?を具体的に見てみる。
// 正規化前
[
{ id: 1, name: tanaka },
{ id: 2, name: sato },
];
正規化前は、1つのデータ構造で、下記の2つの情報を持っている。
- 各 User の情報
- 各 User の順番
これを正規化すると、
// 正規化後
{
data: {
1: { id: 1, name: tanaka },
2: { id: 2, name: sato },
},
allIDs: [1, 2]
...
}
となり、1 と 2 の情報が分離される。ここで、entity が 1 の情報で、domain が 2 の情報となる。
公式 Doc: Basic Reducer Structure · Redux
- Domain, App, UI の3つの State に大きく分ける。
- Domain: entites 的なやつ。 Raw なデータ
- App: アプリの振る舞いに依存するもの。Selected や Loading など。
- UI: Presentetional 層で表示に特化したもの。
- 「stare の形」のことを shape と呼ぶ。
- 「スライス」=「Store におけるサブツリー」のことを指す
State 設計にまつわる記事を読む
他にも記事をいろいろ探して読んでみた。
スライド: redux の state 設計の話 - スタートアップテック
- DomainState と UIState で State を分ける(画面かドメイン)。それぞれの分け方でメリデメがあるので整理。
- 大きく、/entites、/domains、/ui の3つのツリーの種類に分けてる。
- entites への正規化では、id を key にしたオブジェクト化をしてる。またリレーションデータは実データではなくて、リレーショナルに ID で参照。
スライド: Redux の State 設計のお話 - Retty
- API の Reponse と State との観点で、State を考える話。
- シンプルなアプリなら
[Responseデータ] -> [State]
でよいが、複雑なアプリ・データでは愚直に 1vs1 対応をすると非正規化が発生する。 - RDS 的にデータを保持させるのがベターとの判断。
- 「Element 層」というのを作って API の変化に対応。
[API] → |FRONTEND| → [Response] → [Element] → [View]
Redux Architecture Guidelines を読んでの所感
- redux/action 設計についての基本的な考え。
- オブジェクトの入れ子を避ける
- UI 用データを State に保存しないで、Raw データを入れる
Twitter 公式サイトの Redux Store 設計を少しだけ読み解いてみる
- Twitter の State 設計を見て、設計を考察。
- normalizer というライブラリで Res を正規化。https://github.com/paularmstrong/normalizr
- /entites の配下にて ResponseRaw データ管理。
- /entities 配下にて、loading 管理をしている。
Dissecting Twitter’s Redux Store - Statuscode - Medium
- /entities/tweets/entities に、1つ1つのツイートの Raw データ
- /homeTimelines/timeline に、タイムラインのデータを格納
React/Redux の設計に関する参考記事まとめ - dackdive’s blog
- この記事みたいなの。自分が見つけられなかった記事がいろいろあったので、助かった。
あとがき
まとめ
- 正規化をする。リストからオブジェクトにしてアクセッサビリティを担保する。DRY を重視して、リレーショナルに設計する
- 公式では loading ステートは AppState だが、Twitter では Entities 配下にあったりするので、ある程度は選択して決める
所感
Context から Redux へのリプレイスで State 周りを色々調べたが、「そもそも Context での State 設計をよりきちんと行えれば Redux へのリプレイスせずとも良かったのでは?」というのが一通り開発が終わってからの印象。
- ReduxWay を進める派
個人的に Context と Redux の併用は、Redux の思想である Single Source of Truth に反するのでやるべきではないと考えてます。UIに関する状態のみを State Hook で持たせ、あとは Redux で一貫して管理というのが初心者も混乱せずいいと思ってます。
— 大岡由佳『りあクト!🔥Firebase編』Boothで好評販売中! (@oukayuka) May 2, 2019
- ContextWay を進める派
hooks時代のreactは基本的にanti single global stateのようで、local states+contextsが基本だと思います。reduxの出番はactionによるseparation of concernsが必要になる大規模アプリに限定されると思います。reduxの用途は無くなりませんが、これまではoveruseされたという解釈です。
— Daishi Kato (@dai_shi) May 2, 2019
ReduxWay による State 設計は、ひとまずのベストプラクティスが確立されているように思える。が、ReactHooks については、登場したばかりで情報が少ないので、設計指針がわからなかった。
「ローカルで表現できるものは useState で、グローバルにしないと表現が厳しいものは Context に、」を指針に、でサービスのグロースに伴って増築を繰り返したら、Context がゴチャツキ出していたので、そのタイミングで Redux へのリプレイスを行ったが、そうではなく Context の State を再設計・整理するべきだったのかもしれない。それこそ、Redux みたいに entities・domains・ui みたいに責務を分割させたり?とか。
とはいえ、Redux にする選択をしても、今では react-redux に useDispatch / useSelector があるので connect/mapStateToProps/ mapDispatchToProps が必要なく、記述量もだいぶ少なく済むし、devTool も優秀だ。
枯れた技術の Redux か、新しい技術の Hooks か、でメリデメがあるので、どう設計するのか?はある程度は決めの問題もありそうかな、という印象でした。
機会があれば次は Context による State 設計について調べたいですね。