こんにちは。菅野です。
Reactと静的型付け言語は最高ですよね!(唐突)
既に使いこなしてる人も、今最高だと理解した人もいると思います。
ただ、今現在の社内ではVue.jsとBabelを使ったES環境が主流でReact派は悲しい思いをしています。1
そこで私は社内向けの小物ツールをReactで自作してReact欲を発散していたりします。
どんなツールかというと、タスクの見積もりで行うプランニングポーカーをちょこっと省力化するもので
コイツを社内にじんわり布教しています。フフフ…
このポンコツツールで使用されているものは、
Scala.js + React + Akka HTTPでWebSocketという闇の技術で、
そんな物をちゃっかり開発用サーバで動かすという投げやりな運用をしています。
しかしScala.js + Reactがちょっと辛くなってきたのと、やっぱりエコシステムの大きなフツーのReactがやりたくなってしまいました。
そこで現状のダークな感じから、React + TypeScript + Firebaseというお洒落な技術2に大変身させてみました。
今回は私が今時のReact + Redux + TypeScriptの構成を学びながら、
乱立する周辺ツールやライブラリの中からどのようなものを使ったのかをご紹介したいと思います。
開発環境
パッケージマネージャー: Yarn
パッケージマネージャーはnpmの代わりに互換性があるYarnを使いました。
npmと比べると依存パッケージを追加する yarn add
の動作が素敵だったり、yarn outdated
の表示がnpmより親切だったりして、あえてnpmを使う理由が無いと思っています。
モジュールバンドラー: webpack
最近はRollup、FuseBox、Parcelなど後発の良さげなものがたくさんありますが、充実したプラグインと情報を決め手にしてwebpackを選びました。慣れの問題
言語: TypeScript
ECMAScriptのスーパーセットで静的型付けの言語で、一度慣れるとこれ以外でJSの開発はしたくなくなる素晴らしい言語です。
現在のESの機能はTypeScriptでも使える上、JSXにも対応していて、出力はES5にトランスパイルすることが出来るパーフェクトぶりのため今回のアプリ開発ではBabelを使用していません。
webpackにts-loaderを組み込むだけで使用することが出来ます。もう素のJSには戻れません!
CSSフレームワーク: material-ui
単純に好きなものを使えば良いと思いますが、bootstrapよりオシャレなので採用しました。
ReactというかRedux環境
Redux
状態を管理するフレームワークです。
Reactでの状態の管理の仕方はいろいろあると思いますが、
今回私は全ての邪念を取り払ってアプリの全ての状態をReduxで管理するようにしました。
そうすることによってアプリのロジックは殆どがReduxに集約され、コンポーネントはシンプルなSFC(Stateless Functional Components)だけにすることが出来ました。
コンポーネントがただの関数になるためTypeScriptで冗長になりがちな部分が無くなってとてもスッキリします。
interface StateProps { hoge: string; } function MyComponent({ hoge }: StateProps) { return ( <div> {hoge} </div> ); }
react-router-redux
react-routerをreduxと接続するMiddlewareです。
ReduxでURL遷移のActionを扱うことが出来るようになります。
redux-form
reduxに接続されたFormを簡単に作れるライブラリです。
専用のreducerに値が同期されるので入力値を簡単に取り回すことが出来ます。
リアルタイムや非同期のバリデーション、入力値リセット等の機能も豊富で、
これを使わずにフォームは作りたくなくなる一品です。
typescript-fsa, typescript-fsa-reducers
なんということでしょう!あの冗長だったActionCreatorとReducerがスッキリしました。
const CHANGE_HOGE = "CHANGE_HOGE"; export function changeHoge(payload: string) { return { type: HOGE, payload } } export function fugaReducer(state: State = initialState, action: Action) { switch (action.type) { case CHANGE_HOGE: return {...state, ...{ "hoge": action.payload }}; default: return state; } }
こんなのが
const actionCreator = actionCreatorFactory(); export const changeHoge = actionCreator<string>("CHANGE_HOGE"); export const fugaReducer = reducerWithInitialState<State>(initialState) .case(changeHoge, (state, payload) => ({...state, "hoge": payload})) .build();
これで済みます。
非同期Action用Middleware: redux-observable
redux-thunkが力不足になってきてredux-sagaが流行ってきていますが、個人的にはちょっと読みづらいなと思っています。
RxJSにアレルギーがない場合には代わりにredux-observableがオススメです。
RxJSのパワーでActionのストリームの変形を行う用な感じで副作用を書けます。
Promiseや非同期functionとの相性も良くて、RxJsの豊富なOperatorによって非同期処理を自由自在に書くことが出来ます。
結局のところ同じことをredux-sagaでも出来ますが、こちらのほうがぱっと見で何をしようとしているのかが分かりやすいように感じます。
redux-observable用ユーティリティ: typescript-fsa-redux-observable
TypeScriptでredux-observableを使うときの旨味が増します。
具体的にはActionsObservableに ofAction
メソッドが生えます。
ofAction
にtypescript-fsaで作成したActionを引数で渡すことで対応するActionを購読することが出来ます。
また、素のredux-observableのメソッドと違ってActionの型情報が生きていて素晴らしいです!
const fooEpic: Epic<Action<string>, State> = (action$, store) => action$.ofAction(foo) .distinctUntilKeyChanged("payload") .mergeMap(({ payload }) => someAsyncRequest(payload).then(x => bar(x))) .retry(2) .catch(e => Observable.of(showError(e.message)));
reselect
ReduxのStoreから状態を取り出して目的の値を計算する関数を定義するためのライブラリです。
それだけだと普通の関数を用意すれば良いのですが、reselectを使うと自動的にメモ化をしてくれます。
Storeの状態が変わっていなくて再計算する必要がないものに関しては以前の値を返すようなるため、Storeから画面に表示する情報が多い場合には効いてくると思います。
この手のものは後から入れるのが大変だと思うので、そこそこの規模になる場合ははじめから入れておくのが良いと思います。また、共通のロジックをまとめたり、表示コンポーネントに計算ロジックをあまり入れないように意識するようになる効果も期待できると思います。
私はコンポーネントとStoreは疎結合になるようにして、Reduxとの接続部分にてコンポーネントが必要とする値を計算してpropsに渡すように作りました。
こうすることでコンポーネントの機能も見栄えもスッキリして、テストに関しても表示を確認するだけで済むようになりました。
Ducks
これはライブラリではなく、ファイル構成のやり方です。
いつものやり方だとReduxで使う部品は constants
actions
reducers
のようなディレクトリで区切ってそれぞれにパーツを置くような作りをすると思います。
ただ、実際には同じ関心事のパーツを一度に開発するので、actionsとreducersを行ったり来たりで結構やりづらかったりします。
Ducksでは modules
というディレクトリの中で同じ関心事をモジュールという単位で一ファイルにまとめます。
例えば認証ユーザーに関するモジュールと言うように、以下のような感じでまとめています。
const actionCreator = actionCreatorFactory("AUTH"); const loginAsync = actionCreator.async<{}, User, Error>("LOGIN"); export const login = loginAsync.started; const loginDone = loginAsync.done; const loginFailed = loginAsync.failed; export const authReducer = reducerWithInitialState<State>(initialState) .case(loginDone, (state, payload) => ({...state, "loginUser": payload.result})) .build(); const loginEpic: Epic<Action<Success<{}, User> | Failure<{}, Error>>, State> = (action$) => action$.ofAction(login) .mergeMap(() => { return asyncLogin() .then(result => loginDone({"params": {}, "result": result.user})) .catch(e => loginFailed({"params": {}, "error": e})); }); const loginFailedEpic: Epic<Action<Error>, State> = (action$) => action$.ofAction(loginFailed) .map(({ payload }) => showError(payload.error)); export const epics = combineEpics(loginEpic, loginFailedEpic);
ホスティング
Firebase
いわゆるmBaaSです。
ホスティングもJSのSDKもあるためSPAのサイトを作ることも出来ます!
リアルタイムアップデートに対応したDBだけでなく、バックエンドで関数を動かしたりも出来て文句なしに最強クラスです。
Webコンソールでプロジェクトを作成し、手元のJSプロジェクトで
yarn add firebase-tools
yarn firebase login
yarn firebase init
とやるだけでFirebaseを使ったアプリの開発が始められます!
これから始まる開発を暗示するかのようにFIREBASEが大炎上します。
その他
Storybook
コンポーネントの確認が簡単にできるツールです。
Firebaseアプリを作る場合はAPIキーの情報をアプリ内で持っておくこともできます。
しかし、どのFirebaseプロジェクトのホスティング環境にデプロイしても動作するようにするためには決まったURLから環境情報を取得しなければならず、そのような作りの場合は動作確認にwebpack-dev-serverなどは使えずfirebase-toolsのホスティングのエミュレーションを使う必要があります。
ここで困るのがHMRで、動的リロードが出来ないと表示コンポーネントの開発は一気に辛みが増します。
最低限のコンポーネントのチェックが出来れば良いので、変に頑張るよりもコンポーネントの開発・表示の確認は全てStorybookで賄いました。はじめからHMRに対応しているのでソースを書き換えてTypeScriptのコンパイル後に自動的に表示が更新されるため開発がサクサク進みます。
また、Storybookを見ながらコンポーネントを開発するとReduxのStoreに惑わされずに本当に欲しいデータをpropsに渡すように設計しやすいと感じました。
ちなみにredux-formのコンポーネントはStoreに接続されていないと動作しないので、
上手く分離する必要がありました…。
storybook addon: knobs
コンポーネントのpropsに入れる値を簡単に弄れるようになるStorybookのアドオンです。
リストや表示の切り替えに関わるpropsの値を弄って表示の確認が出来るので捗ります。
新しいESの機能いろいろ
言語の機能ですが強力なので紹介します。
TypeScriptではESの機能が普通に使えるため、分割代入、スプレッド演算子、非同期関数などがそのまま使えます。
propsを分割代入で変数に割り当てたり、スプレッド演算子を使ってオブジェクトや配列の一部上書きコピーによるstateの安全な書き換えを簡潔に実現できます。
非同期関数に関してもPromiseが大量に出てきたときの選択肢の一つとして心強いです。
const queryEpic: Epic<Action<string>, State> = (action$) => action$.ofAction(query) .mergeMap(async ({ payload }) => { const resultA = await queryAsyncA(payload); const resultB = await queryAsyncB(resultA); const resultC = await queryAsyncC(resultB); return showResult(resultC); });
終わり
紹介できたのは私が小物ツールを作るために調べたりしたことの一部でしたが、
同じようなものを作る人のライブラリ選定の参考になればと思います!