前提知識
fluxをある程度
経緯
ある日typescript-fsaの話をしました。
import actionCreatorFactory from 'typescript-fsa';
const actionCreator = actionCreatorFactory();
const a = actionCreator('A');
const b = actionCreator('A');
たとえばreducerが2つあって、一番ベーシックな書き方の場合は、a.type
とb.type
とかでswitch文を書くかと思います。
今回の場合、aのtypeはA
で、bのtypeはB
です。
このa又はbのactionを送出した場合、2つのreducerは両方反応してしまうのはわかりきっていることだと思います。
しかし、とある方から反応しないと思うよ!と意見を頂きました。
一応typescript-fsa
の実装コードも読んでたのでそんなはずはない!と思ったんですよね。
でも、もしかしたら凄い魔法がかかってる可能性も無きにしもあらずですし、憶測だけで話すのもよくないので、ちゃんと検証しようと思い、それっぽいコード書いてconsoleでログを出しながら実行して検証してみました。
https://github.com/k-okina/research-redux-typescript-fsa
結果、やはり両方反応しました。
しかし、それってイケてないよね。ライブラリのactionCreator
から生成されるactionCreator
は、全て違うものとして識別して欲しいよねって意見を頂いたので、果たして本当にその必要があるのか?🤔と考察する機会を得られたのでその考察内容を共有したいと思います。
typescript-fsaの仕事
typescript-fsa
はただ、flux standard action
、略してfsa
の仕様に沿ったactionCreatorが作れるよ!ってのが責務です。
fsaの仕様とかにはactionCreatorから作られるactionのactionTypeは唯一でなければならないとは書かれていません。なのでtypescript-fsaにactionTypeの管理も任せたいってのはお門違いな話だと思います。
また、唯一にしたいのって、その人個人、又は特定条件下のプロジェクトだけの話の可能性もあります。
皆本当にそんな事して欲しい?🤔本当に他の反応しないでほしいの?🤔
検証
実際に偶然actionTypeが一致してしまった場合のサンプルを用意しました。
以下がそれです。
import actionCreatorFactory from 'typescript-fsa'
const actionCreator = actionCreatorFactory()
const actionType = 'THIS_IS_ACTION_TYPE'
export const a = actionCreator<{foo: string}>(actionType)
export const b = actionCreator<{bar: string}>(actionType)
import {Action} from 'redux'
import {isType} from 'typescript-fsa'
import {a} from './actions'
type State = {foo: string}
export const reducerA = (state: State, action: Action): State => {
if (isType(action, a)) {
// action.payload is inferred as {foo: string}
return {foo: action.payload.foo}
}
return state
}
import {Action} from 'redux'
import {isType} from 'typescript-fsa'
import {b} from './actions'
type State = {bar: string}
export const reducerB = (state: State, action: Action): State => {
if (isType(action, b)) {
// action.payload is inferred as {bar: string}
return {bar: action.payload.bar}
}
return state
}
import reducerA from './reducerA'
import reducerB from './reducerB'
import {createStore, combineReducers} from 'redux'
export default createStore(combineReducers({reducerA, reducerB}))
例えばこの2つのサンプルreducerをcombineして作られたstoreに対して以下のようなactionを投げてみます。
import store from './store'
import {a} from './action'
store.dispatch(a({ foo: 'hello world' }))
actions.ts
ファイルを見ると分かる通り、2つのactionは全く同じaction typeです。
ですので、reducerA
とreducerB
両方が反応しますが、reducerB
の方はpayload.bar
を期待しているのに、undefined参照をしてしまいます。
以下がその解説です。
import {Action} from 'redux'
import {isType} from 'typescript-fsa'
import {b} from './actions'
type State = {bar: string}
export const reducerB = (state: State, action: Action): State => {
if (isType(action, b)) {
// action.payload is inferred as {bar: string}
// But received action is not a b action!!
// So referenced to undefined variable!!! 😰
return {bar: action.payload.bar}
}
return state
}
これは本来意図しない結果です。
もし同一actionTypeを使いたいなら、UnionTypeをしないといけないのが現実です。
export const correctAction = actionCreator<{bar: string} | {foo: string}>(actionType)
まとめ
つまり、同一actionType
を使って複数のaction
を作る事は一切無さそうです。
せっかくtypescript
で型付けしても簡単に壊される可能性があります。
なので、actionCreator
レイヤーで守るか、actionTypeGenerator
を用意し、その中で守る必要がありそうです。
typescript-fsa
ではactionCreatorFactory
でprefix
を渡してactionType
を調整したりすることもあるので、多分一番現実的な解決方法は、actionCreator
内でactionType
にid
採番をするとかでしょうか。