この記事は 2019 年の React Native Advent Calendar 25 日目の記事です。
TL;DR
React Native のデータフローは Redux が扱いやすいが、 switch
形式の Reducer だと計算量が最悪 O(n) になってしまうため、 テーブル参照形式を採用して O(1) としたい。このテクニックは Redux 公式で紹介されているが、これを TypeScript で使いたい。その実装例を紹介する。
というようなことを考えるきっかけとなったのは React Native の本の執筆をしているから。来年春までに出る予定。
React Native のデータフロー
クリーンアーキテクチャー的にデータとその振る舞いを Entity や Usecase として閉じ込めておくにあたり、現状で Redux が最適だな、と感じています。 React State や useReducer
は UI や Framework によりすぎていて、スマートな UI を助長するものなので必要最低限の使用に留めるのがよいでしょう。
「実装スピードが…」という方はこちらのスライドを読んでみてください。
switch
版 Reducer の問題点
switch
はただの if
文の連なりを少し見通しよく書けるというだけです。すべての case
が上から順にマッチするまで判定されます。次のふたつは等価です。
switch (action.type) {
case ADD:
break;
case SUB:
break;
default:
break;
}
if (action.type === ADD) {
} else if (action.type === SUB) {
} else {
}
これだと Action Type が増えていくとマッチまで余計な判定が増えちゃうよね、しかも毎回、というのがざっくり問題になるわけです。まあ実用上そんなに問題にならないんですが、高速化できるところはしたいというモチベーション。
解決策
Action Type だけの判定なんだからキーに Action Type 、値にそれぞれの Action の reduce 処理を持った Object を定義してやれば handlers[action.type](state, action)
みたいに呼び出せるじゃん、というのがアイディアです。
で、それを JavaScript で定義した場合のコードが次です。公式から引っ張ってきています。
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
わかりやすいですね。
型付けしよう
React Native も TypeScript 対応して長いので、じゃあ TypeScript で同様の関数を書こう、となるじゃないですか。が、一筋縄では行かないんですよこれが。
とりあえず眠いので現時点の到達点をどーん。
import * as Redux from 'redux'
type Handler<State, Action> = (state: State, action: Action) => State
type Handlers<State, Actions> = {
[Type in keyof Actions]: Handler<State, Actions[Type]>
}
interface TypeActionSet {
[key: string]: any
}
export default function createReducer<
State,
Actions extends TypeActionSet,
HandlerDefinitions extends Handlers<State, Actions> = Handlers<State, Actions>
>(initialState: State, handlers: HandlerDefinitions): Redux.Reducer<State> {
return function(state = initialState, action): State {
return handlers.hasOwnProperty(action.type)
? (handlers[action.type] as Handler<State, Actions[typeof action.type]>)(
state,
action as Actions[typeof action.type],
)
: state
}
}
Action Type に Symbol を使うのはあまり旨味がないので文字列だけに限定しています。
any
を使っているので警察に逮捕されてしまいますね。さらに as
でキャストしているのでお行儀の良いコードではありません。
また、こう使う感じになるのでちょっと大変です。
type State = ReturnType<typeof createInitialState>
interface TypeActionSet {
[ADD]: ReturnType<typeof add>
[SUB]: ReturnType<typeof sub>
}
export default createReducer<State, TypeActionSet>(createInitialState(), {
[ADD]: (state, action) => state + action.payload.plus,
[SUB]: (state, action) => state - action.payload.minus,
}
課題として次が残っています。
-
any
撲滅 - キャスト撲滅
- TypeActionSet
Action は type
プロパティが存在すること以外の規定がないので、実質 any
と同様です。つまり 1, 2 は撲滅が難しそうです。また、 3 は Action の型から Action Type と Action の組の型を導出することができるのか?ということになります。 TypeScript Compiler API を使えば自動で吐き出すことはできそうですが…。
これで良ければ適度に使ってみてください。
ちなみに
本家が出している Redux Toolkit で createReducer
として同様のものが提供されていますが、 Reducer に渡ってくる action
が any
となっていて例えば次のコードを書いても怒ってくれません。
export default createReducer<State, TypeActionSet>(createInitialState(), {
[ADD]: (state, action) => state + action.payload.plus,
// 指定するプロパティ名を間違えてしまった!
[SUB]: (state, action) => state - action.payload.plus,
}
コレジャナイ感がすごかったので、自前で考えることにしたのでした。
宣伝
さて、なんでこんなことを考えたのかと言うと、本を書いていて、 Redux の章を僕が担当したからです。執筆陣は @YutamaKotaro 、 @nitaking 、僕です。技術評論社さんから出版していただく予定です。
対象読者はもちろん React Native でアプリを作りたい方です。次のスキルセットがあれば読める本としています。
- JavaScript(ECMASCript 5)
- HTML
- CSS
- Git
目次は次の予定ですが、まだ変動する可能性があります。
- React.js / React Nativeの概要とその背景
- TypeScriptとECMAScript 2015
- 開発環境の構築
- React Nativeの基本
- 作成するアプリケーションの仕様策定
- テストによる設計の質の向上
- Navigationの概要と実装
- Atomic Designとコンポーネントの実装
- データフローの設計および実装
- FireBaseを使ったバックエンド連携
- E2Eテスト
- アプリストアへの公開
Lean Core 構想も軌道に乗り React Hooks も馴染みつつある今、まあいい頃合いだよね、いっちょ書いてみっか、という感じです。 3 月初旬出版を目指して執筆陣、編集者さんともに頑張っていますので乞うご期待。
このニュースがみなさんへのクリスマスプレゼントになるよう祈っています。メリークリスマス!