Reduxを採用しているアプリケーション向けの記事です。
Reduxの副作用処理で使われるMiddlewareといえば、公式サンプルではredux-thunkが使われていて、redux-sagaもよく使われているという印象があります。
redux-thunkよりredux-sagaが選ばれる理由の1つは、副作用処理を分離でき、ActionCreatorを純粋関数(副作用のない関数)に保てるところかと思います。
redux-thunkではこういうActionCreatorになり、redux-sagaだとこういうActionCreatorになり、加えてこういうSagaを作ります。
redux-thunkでテストを書くとActionCreatorはこういう非同期処理をモックしたコードになり、mapDispatchToPropsはこういうコードになります。
redux-sagaだとActionCreatorはこういうコードになり、Sagaはこういうコードになり、mapDispatchToPropsはこういうコードになります。
このように副作用処理を分離するMiddlewareは他にも色々ありますが、redux-elm-middlewareという、AltJSのElmを利用するMiddlewareがあったので、試しに使ってみました。
Elmとは
- 型安全、Null安全によりランタイムエラーが無い言語
- すべての型が不変値
- 副作用はElmランタイムが処理し、コードはすべて純粋関数
- ハイパフォーマンスなJavaScriptを生成する
- 採用事例増加中
A small but noticeable number of developers are starting to choose Elm over JavaScript.
Front-End Developer Handbook 2017
- そんな難しくない文法(ElmとJavaScriptのシンタックス比較表)
redux-elm-middlewareとは
JSからElmにActionを渡し、Elmで更新されたStateをJSで受け取るMiddlewareと、そのStateを適用するReducerを提供します。
元々ReduxはElmをインスパイアして作られたので、状態が不変であるとか、Reducerが純粋関数であるとかのRedux原則はElmの言語仕様だったりするので、相性はそんなに悪くないです。が、正直無理して組み合わせるよりはElm単独で使った方がいいかなとは思います。
とはいえ、Elmで1から作るのは新規開発でもないとなかなかハードルが高いので、このMiddlewareで部分的に既存の処理を置き換えるというのならありじゃないかなと思っています。
Counterを実装してみる
※この先Elmコードがそこそこ出てきますが、あまり細かい説明をしていませんので、完璧に読めないと気が済まない方は先に記事の一番下のリンクをご参照ください。
こちらのsagaで作ったCounterサンプルをベースにします。
こちらを参考にelmをインストールします。
elm-loaderを追加します。
$ yarn add -D elm-webpack-loader
redux-elm-middlewareを追加します。
$ yarn add redux-elm-middleware
Counter.jsのmapDispatchToPropsを変更します。
ActionCreatorを介すことは無くなるので、直接Actionをdispatchします。
export const mapDispatchToProps = (dispatch: Dispatch) => ({
onIncrease: () => {
dispatch({ type: "INCREASE" })
},
onIncreaseIfOdd: () => {
dispatch({ type: "INCREASE_IF_ODD" })
},
onIncreaseAsync: () => {
dispatch({ type: "INCREASE_ASYNC" })
},
onDecrease: () => {
dispatch({ type: "DECREASE" })
}
})
次に、modules/count.jsでやっている処理をElmに移植します。
ElmにはPortsという、JSと値をやり取りするためのPubSubの仕組みがあり、
redux-elm-middlewareでは、dispatchされてきたActionTypeと同名のElmのPortにActionを送信し、更新された値を受信します。
同名と書きましたが、正確にはcamelCaseに変換した文字列になります。
{ type: "INCREASE" }
であれば、以下のようにPortを定義します。
port increase : (Value -> msg) -> Sub msg
ElmでのActionにあたるMsgを定義します。
type Msg
= Increase
値の変更を監視するsubscriptionsにて、increaseが値を受け取ったら、Increaseを発行する処理を定義します。
subscriptions model =
increase <| always Increase
ElmでのStateにあたるModelの型エイリアスを定義します。
type alias Model =
Int
ElmでのReducerにあたるupdateを定義します。
update msg model =
case msg of
Increase ->
( model + 1, Cmd.none )
Elmアプリケーションの初期化関数initを定義します。
init =
( 0, Cmd.none )
ModelをJSでの型に変換する関数encodeを定義します。
encode model =
int model
Elmランタイムにアプリケーションを登録します。
main =
Redux.program
{ init = init
, update = update
, encode = encode
, subscriptions = subscriptions
}
こうして作ったReducer.elmを、redux-elm-middlewareのcreateElmMiddlewareとcreateElmReducerを使ってJS側でReducerとして扱います。
// @flow
import { applyMiddleware, combineReducer, createStore } from "redux"
import createElmMiddleware, { createElmReducer } from "redux-elm-middleware"
import Elm from "./modules/Reducer.elm"
const initialState = 0
const countReducer = createElmReducer(initialState)
const rootReducer = combineReducers({
count: countReducer
})
const { run, elmMiddleware } = createElmMiddleware(Elm.Reducer.worker())
const store = createStore(
rootReducer,
preloadedState,
applyMiddleware(elmMiddleware)
)
run(store)
これが一通りの実装方法になります。
例えば{ type: "INCREASE_ASYNC" }
のような非同期処理のあるActionの場合は、以下のようにModelとセットで非同期処理の関数を返すことで、ランタイム側で処理してくれます。
update msg model =
case msg of
Increase ->
( model + 1, Cmd.none )
IncreaseAsync ->
( model, increaseInSecond )
increaseInSecond : Cmd Msg
increaseInSecond =
setTimeout Increase 1000
setTimeout : Msg -> Float -> Cmd Msg
setTimeout msg delay =
perform (always msg) (sleep (delay * millisecond))
テストは純粋関数ですのでシンプルになります。
suite =
describe "Reducer Test Suite"
[ describe "init"
[ it "returns initial model" <|
expect init to equal ( 0, Cmd.none )
]
, describe "update"
[ it "returns increased model" <|
expect (update Increase 0) to equal ( 1, Cmd.none )
, it "returns model and command that increases in a second" <|
expect (update IncreaseAsync 1) to equal ( 1, increaseInSecond )
]
]
こんな要領で実装したCounterをこちらに置いてありますので、興味があればごらんください。
また、もう少し本格的なアプリケーション開発に使えそうなボイラープレートも作っていますので、よかったらこちらもみてみてください。
もっとElmを知りたいと思ったら
この辺りを見るといいかもしれません。
- Ellie - オンラインREPL
- Elm Examples
- Get Started
- Elm Tutorial - 日本語あります