Help us understand the problem. What is going on with this article?

型安全な createReducer を求めて & 宣伝

この記事は 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 {

}

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,
}

課題として次が残っています。

  1. any 撲滅
  2. キャスト撲滅
  3. TypeActionSet

Action は type プロパティが存在すること以外の規定がないので、実質 any と同様です。つまり 1, 2 は撲滅が難しそうです。また、 3 は Action の型から Action Type と Action の組の型を導出することができるのか?ということになります。 TypeScript Compiler API を使えば自動で吐き出すことはできそうですが…。

これで良ければ適度に使ってみてください。

ちなみに

本家が出している Redux ToolkitcreateReducer として同様のものが提供されていますが、 Reducer に渡ってくる actionany となっていて例えば次のコードを書いても怒ってくれません。

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

目次は次の予定ですが、まだ変動する可能性があります。

  1. React.js / React Nativeの概要とその背景
  2. TypeScriptとECMAScript 2015
  3. 開発環境の構築
  4. React Nativeの基本
  5. 作成するアプリケーションの仕様策定
  6. テストによる設計の質の向上
  7. Navigationの概要と実装
  8. Atomic Designとコンポーネントの実装
  9. データフローの設計および実装
  10. FireBaseを使ったバックエンド連携
  11. E2Eテスト
  12. アプリストアへの公開

Lean Core 構想も軌道に乗り React Hooks も馴染みつつある今、まあいい頃合いだよね、いっちょ書いてみっか、という感じです。 3 月初旬出版を目指して執筆陣、編集者さんともに頑張っていますので乞うご期待。

このニュースがみなさんへのクリスマスプレゼントになるよう祈っています。メリークリスマス!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした