Hooks時代のReduxについての感想下書き
TL: DR (個人的感想)
相変わらず有益なのはcombineReducer。これが使いたいかどうかで決まりそう
気をつけてればuseContext自前でも行ける気がする(行けると信じたい)
簡易な非同期通信のためのmiddlewareはオサラバしたい(できるとうれしい)
背景要因
hooks
useReducer / useContexができた
Context APIがstableになった
Redux
useDispatch / useSelectorなどが増えた
batch関数ができた
JavaScript
awaitが広まってきた。
箇条書き
嬉しみ1:connectと付き合わなくて良い
表示に影響しないロジックをコンポーネントに与えるためにHoCsがこれまで多用されていた
ConnectもHoCsの一つ
これがhooksに置き換えられるようになった
HoCsは読みづらく、型が付けづらかった
hooksのおかけでconnectを書かなくて良い。
嬉しみ2:selector
mapStateToPropsでのデータ取得。概ね複雑になりがちだった。
そこらへんをまとめ上げ、memo化するのがselector。
Reduxは自身の責務からは外してreselectに移譲していた
個人的にはselectorのレイヤーを意識するかどうかでstateの設計が変わるので、もう少し公式が取り入れる概念ではと感じていた。
useSelectorでこれがほぼ取り入れられた形なのがわりと嬉しい
嬉しみ3:action creatorをサボる
(多分行ける気がするけどなんか落とし穴あるかも)
useDispatchで直接actionを投げる関数を作れば良い。のでactionCreator作成の煩雑さを避けれる
これまでは共通化のポイントがaction creatorだったが、hooksで共通化して使い回せるはず。
code:js
const useMyActions = () => {
const dispatch = useDispatch()
const increment = () => {
dispatch({ type: "INCREMENT" })
}
const decrement = () => {
dispatch({ type: "DECREMENT" })
}
return {
increment,
decrement,
}
}
// actionを使いたいコンポーネントでhooksを利用する
const MyIncrementButton = () => {
const { increment } = useMyActions()
return <button onClick={increment}>Up!</button>
}
嬉しみ4:非同期通信関連
useEffectやawaitを組み合わせたactionで定義することで十分回避出来る
(↑良さそうな例なので拝借)
actionとしてasyncな関数(promiseでも良い)を使う。
複数の非同期処理を扱うようなものはbatchを使うで十分叶う
middlewareは長年頭痛の種だった。
thunk、promise、saga、observable、、、
シンプルなものだとロジックが散るし、大きいものだとまた複雑だし・・・
個人的にobservableは大好きだったが、他者に勧めれるものでもないというのがあった
(observableに触る機会が減りそうなのは悲しいが、Epicの考え方はいろんな他のライブラリに伝播していったしまあいいかというきもち)
Context + useReducerと比べるとどうか?
使わなくていいならReduxを使わないで行きたい
半分ぐらい私怨。ずっとReduxキラーを待っていた・・・!
Context + useReducerで行くことになる
やってみたところ、Context + useReducerもそこそこボイラープレートな感じがあって一周回ってreduxの方が楽では?と思わなくない。
懸念:Context + useReducerは複雑になる?
十分ありえる。
でも割となんとかなりそうな予感もしてる
(やってみないとわからん)
Pure React Hooksでやることの利点
他のエコシステムに乗りやすい
再利用性が高まりやすい
コンポーネントの独立性を高めやすい
Contextでやっていくなら気をつけたいとこ
各Stateは独立させる。依存関係を作らない
例えば各StateはAPIの結果を素直に入れるだけにする、など。
Redux以前のFlux時代にはwaitForでこんがらがる世界が広がっていた。この過ちを繰り返さない
依存関係はselectorの部分で行う方が良い
重複してStateを持たせない
当たり前だけど、同じデータを複数箇所に持たせるのは混乱するので危険。
キャッシュするような場所も、概ねuseMemoなどで叶うはず。
Contextは隠蔽する
hooks使うとき割とこんな感じでContextに触らせないように意識してる
code:js
const MyContext = createContext(null)
export const MyContextProvider = ({ someValue, children }) => {
return <MyContext.Provider value={someValue}>{children}</MyContext.Provider>
}
export const useMyContext = () => {
// 本当はここでcontextのnullチェック(後述)
return useMyContext(MyContext)
}
初期値が入ってないときに怒るように仕掛けをする
↑の続き。
code:js
export const useMyContext = () => {
const context = useMyContext(MyContext)
if(context === null){
throw new Error("初期化されてないよ!")
}
return context
}
reduxもわりと同じことやってる
Contextではカバーし辛いこと
combineReducer
combineReducerの機能部分は、useReducerで扱うことは難しい。
複数のuseReducerをファサードすることでなんとなくそれらしいことは出来る
code:js
export const useMyStore = () => {
return {
...stateA,
...stateB
}
}
巨大で複雑なstateを構築したい場合はやはりcombineReducerは有用になるかもしれない
デバッグツール
useDebugValueはあるが、やっぱりデバッグ周りのエコシステムはReduxは優れてる
(Reduxがデバッグしやすいように作られている面も大きい)
hooksはエコシステムの範囲としてはデカイので、そのうち更に良いhooks用デバッガが出てくる可能性は無くはないが
middleware
非同期のmiddlewareについては上部で否定気味に書いたが、middlewareの役割はそれだけではない
loggingやlocalstorageへの同期など、「すべてのデータの流れを制御したい」という欲求に応えやすい
多分これはhooks + Contextで散らしてるとめっぽうやりづらくなる事が予測される
一定の規約・制約
あまりReduxは制約が大きくないが、それでもRedux wayを守ることは一定のメリットが生まれる
でも、規約というにはかなり薄くやっていたので、逆にReduxにはもっと強めの縛りを作った方がいいのでは?という気持ちも
ディレクトリ構造とかaction名の規約とか・・・
その他余談
今までは React に Reduxはセットみたいな関係性だった。これは結局崩れた気がする
少なくとも割合は減る。立ち位置が変わった。
必須具合がずいぶんと減った印象がやっぱりある。
最初大ボスだったピッコロが愛嬌のある普通の仲間になったぐらいな感じ
個人的にはもっとReduxは独自のレールを持って行ったほうがいいのでは?と思ったり
redux-starter-kit
なんだかキツイ締め付けの反動ではっちゃけてる印象が否めない・・・
selectorのドット記法とか個人的には「キツイっす」という気持ち
ContextでもReduxでも複数Storeを作ってしまうようなことはある。
このようなことを避けたければglobalに値を扱うreactnが良さそう(でもこれはこれで癖が強そう)
useStoreそのまま使うのもまあ辛いよね、というとこからのstate managementツール
middlewareなどが使えたりselectorが使えたりsubscribeが使えたりdevtoolが使えたり。
ただおそらくグローバルのためではなくローカルで複数利用されることまで想定してるような気がする。
ContextやProviderは提供されてない。おそらく意図的。
react-springが作っているのでそれはそうかも。
でもreact-springで使われてるわけでもなさげ
useStoreは多分使わない
概ねuseDispatch / useSelectorで良いはず。
仮にuseStoreが必要だとするとsubscribeを利用したい場合ぐらいになるが、よっぽどなケースな気がしている