JavaScript
flux
TypeScript
architecture

FluxのactionTypeはなぜ唯一である必要があるのか


前提知識

fluxをある程度


経緯

ある日typescript-fsaの話をしました。

import actionCreatorFactory from 'typescript-fsa';

const actionCreator = actionCreatorFactory();

const a = actionCreator('A');
const b = actionCreator('A');

たとえばreducerが2つあって、一番ベーシックな書き方の場合は、a.typeb.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が一致してしまった場合のサンプルを用意しました。

以下がそれです。


actions.ts

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)



reducerA.ts

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
}



reducerB.ts

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
}



store.ts

import reducerA from './reducerA'

import reducerB from './reducerB'
import {createStore, combineReducers} from 'redux'

export default createStore(combineReducers({reducerA, reducerB}))


例えばこの2つのサンプルreducerをcombineして作られたstoreに対して以下のようなactionを投げてみます。


index.ts

import store from './store'

import {a} from './action'
store.dispatch(a({ foo: 'hello world' }))

actions.tsファイルを見ると分かる通り、2つのactionは全く同じaction typeです。

ですので、reducerAreducerB両方が反応しますが、reducerBの方はpayload.barを期待しているのに、undefined参照をしてしまいます。

以下がその解説です。


reducerB.ts

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ではactionCreatorFactoryprefixを渡してactionTypeを調整したりすることもあるので、多分一番現実的な解決方法は、actionCreator内でactionTypeid採番をするとかでしょうか。