手持ちのredux-observableのboilerplateをTypeScript & Hooks化した

手持ちのredux-observableのboilerplateをTypeScript+Hooks化して公開した時の話。

何か開発を始める時に度々使っていたredux-observableのboilerplateがちょっと古くなってきたので、この際TypeScript化してHooksも使うようにしてrxjsも5系から6系に上げよう!と思いたち、なんとか動くところまで持っていけたので、同じような事をしようとしてる人達と自分の振り返りのために記事として残すことにしました。

ちなみに成果物はこれ↓

https://github.com/kunihiko-t/redux-observable-ts-hooks-boilerplate

とりあえず土台を作る

フルスクラッチで1つ1つ設定など書いていくのが面倒だったので

create-react-app my-app --typescript

してからejectしました。

typescript-eslintなどの設定はこちらを参考にさせていただきました。(実際のところ全部終わってから追加しましたが)

http://watanabeyu.blogspot.com/2019/02/typescript-eslinttypescriptlinteslintai.html

そして既存のファイルを*.js -> *.ts, *.jsx -> *.tsx にしてとりあえず突っ込んでいきました。

型周りのエラーを地道に修正していく

コンポーネント周りなど普通にesで書いてると型定義とかもちろんないのでエラーが出まくります。

例えばこんな感じで書いてるとitemsのlengthがないぞとかで怒られると思います。

const repositoryList = ({ items, total_count, isLoading }) => {
    const totalCountElem = items.length > 0 ? (<div>Total Count: {total_count}</div>) : (<></>)
........

このように全てのパラメータに型を定義していきます。

const repositoryList: React.FC<{ items: GithubRepository[], total_count: number, isLoading: boolean }> = ({ items, total_count, isLoading }) => {
......
}

最初はめんどくさいなーとか思ってやってましたが、ちゃんと型定義することでPropTypesとか書かなくて良くなり無駄なコードが減ってエディタの補完にも優しくフロントでも型あったほうが良いなぁと思うようになりました。

ライブラリの型がない

だいたいの新しめのライブラリには型定義があり、 yarn add @types/ライブラリ名 でインストールできます。

ただ、新しすぎるライブラリや古いライブラリなどは型定義がないものもあるようで、今回Hooksのために入れた react-redux 7.1.0も型定義がありませんでした。

yarn add @tyes/react-redux@7.1.0 とかやってみると、指定されたバージョンの型定義がないからリストから選べって言われてしまいます。

どうしたもんかなと思ったんですが、ライブラリに型定義がない場合は自分で追加することもできるらしく、tsconfig.jsonのtypeRootsプロパティに設定されたディレクトリ以下にreact-redux.d.tsみたいなファイルを作って型定義を行うことで型定義がない関数なども利用可能になるようです。 自分の場合はtypeRootsの定義がこんな感じなので

    "typeRoots": [
      "node_modules/@types",
      "src/types"
    ],

src/types/react-redux.d.tsを作りそこに型定義を書きました。 https://github.com/kunihiko-t/redux-observable-ts-hooks-boilerplate/blob/master/src/types/react-redux.d.ts

typescript-fsaの導入

せっかくTypeScript化したのだからその恩恵をもっと受けようと思いtypescript-fsaを導入しました。

これの何が嬉しいのかというと、これが↓

import keyMirror from 'fbjs/lib/keyMirror';

export const ActionTypes = keyMirror({
    USER_LOGIN_REQUEST: undefined,
    USER_LOGIN_SUCCESS: undefined,
    USER_LOGIN_FAILURE: undefined,
})

export function login() {
    return {
        type: ActionTypes.USER_LOGIN_REQUEST,
        payload: { id: 'test' },
    };
}

こうなり、

const keyMirror = require('fbjs/lib/keyMirror')

export const ActionTypes = keyMirror({
    USER_LOGIN: undefined,
})


import actionCreatorFactory from 'typescript-fsa'

const ac = actionCreatorFactory()

interface LoginParam {
    id: string
}

interface LoginResult {
    data: any
}

interface LoginError {
    error: string
}

export default {
    login: ac.async<LoginParam, LoginResult, LoginError>(ActionTypes.USER_LOGIN),
    }

typescript-fsa-reducersを併せて使うことで reducerもこんな感じで書けるようになってコードとてもスッキリします。

import immutable from 'immutability-helper'
import { reducerWithInitialState } from 'typescript-fsa-reducers'
import actions from '../actions/user'

export const userState = {
    isAuthenticated: false,
    status: 'idle',
}

export default {
    user: reducerWithInitialState(userState)
        .case(actions.login.started, (state, action) => {
            return immutable(state, {
                status: { $set: 'running' },
            })
        })
        .case(actions.login.done, (state, action) => {
            return immutable(state, {
                status: { $set: 'idle' },
                isAuthenticated: { $set: true },
            })
        })
        .case(actions.login.failed, (state, action) => {
            return immutable(state, {
                status: { $set: 'running' },
                isAuthenticated: { $set: false },
            })
        }) ........

今回はredux-observableも利用するのでtypescript-fsa-redux-observableも導入しました。 こちらを利用することで

ofAction(actions.login.started)

のようにepicを書くことができてとても嬉しいのですが、1点注意事項があって、yarn add typescript-fsa-redux-observable でインストールできるtypescript-fsa-redux-observableはちょっと古いようで、githubのrepositoryにあるREADMEのような記述をするにはyarn add https://github.com/m0a/typescript-fsa-redux-observableと直接githubのrepositoryを指定する必要がありました。

Epicの修正 rxjs5->6

自分が使っていたrxjsが5系だったので、ついでだから6系に上げようと思ったら結構仕様が変わっていて、さらにTypeScript化することもあいまってなかなか大変でした。

rxjs5系では . で繋げてこんな感じで書いていたのですが

export function fetchRepositories(action$) {
    return action$
        .ofType(ActionTypes.FETCH_REPOSITORIES_REQUEST)
        .delay(1000)
        .mergeMap(param =>
            Observable.defer(() =>
                Observable.fromPromise(
                    axios.get(
                        `${ApiBaseUrl}/github/repositories?installation_id=${
                            param.payload.installationID
                        }`
                    )
                )
            ).map(data => {
                return {
                    type: ActionTypes.FETCH_REPOSITORIES_SUCCESS,
                    payload: { response: data.data },
                };
            })
        )
        .catch(error =>
            Observable.of({
                type: ActionTypes.FETCH_REPOSITORIES_FAILURE,
                payload: { error },
                error: true,
            })
        );
}

rxjs6 + typescript-fsa-redux-observable ではこんなノリになります。(ちょっと上記と内容が違いますが)

pipeで繋げる感じになって、method chainで呼び出すのではなく、関数を個別で呼び出す感じです。

export const fetchRepositories: Epic<AnyAction> = (action$) => action$.pipe(
    ofAction(actions.fetchRepositories.started),
    debounceTime(1000),
    mergeMap((param) =>
        ajax.getJSON(`https://api.github.com/search/repositories?q=+language:javascript+created:%3E2016-10-01&sort=stars&order=desc`).pipe(
            map(data => {
                return actions.fetchRepositories.done({
                    params: param.payload,
                    result: { repositories: data },
                })
            }),
            catchError(error =>
                Observable.of(actions.fetchRepositories.failed({
                    params: param.payload,
                    error: error,
                })),
            ),
        ),
    ),
)

https://github.com/kunihiko-t/redux-observable-ts-hooks-boilerplate/blob/master/src/epics/github.ts

コードはすっきりしてこの書き方のほうが好きなんですが、型エラーがでた時にエラーメッセージがすさまじく長くて一体どこの型がどうマズイのか非常に分かりにくくて辛かったです。

React ReduxのHooks利用

React側のHooks使うんだから、Reduxでも用意されてるHooksを使いたいと思い、対応している7.1.0-rc.1も出ていたので使っていくことにしました。(この記事執筆時点で丁度7.1.0が出ましたが型定義は7.0.9のまま。。)

導入は割と楽にできて、connectmapStateToPropsが不要になり、代わりにuseSelectorを使うようにしたり、propsから取ってきていたdispatchをuserDispatchから取得したりするだけで簡単に移行できました。

コードを見たほうが早いと思います。 https://github.com/kunihiko-t/redux-observable-ts-hooks-boilerplate/blob/master/src/routes/Home.tsx

やってみた感想

epic周りのエラーを修正している時が一番しんどかったです。

React ReduxのHooksやtype-script-fsaのおかげでコードがシンプルになり、TypeScriptのおかげでエディタの補完機能がいい感じに効くようになるのはかなり良いなぁと思いました。

boilerplate程度の規模だとちょいちょい時間を取って数日もあればできるのでやって良かったなぁという感じです。

最近next.jsでnext exportという機能があると知ったので次はnext.jsで動くようにしたいと思っています。

あと、webpackのalias設定うまくいってないのでそこをどうにかしたい。 TypeScriptだとちょっと勝手が違うぽい。

このエントリーをはてなブックマークに追加
Comments