17
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Reduxで副作用を分離して状態を不変に扱うredux-elm-middlewareを使う

Last updated at Posted at 2017-07-17

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

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します。

containers/Counter.js
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を定義します。

modules/Reducer.elm
port increase : (Value -> msg) -> Sub msg

ElmでのActionにあたるMsgを定義します。

modules/Reducer.elm
type Msg
    = Increase

値の変更を監視するsubscriptionsにて、increaseが値を受け取ったら、Increaseを発行する処理を定義します。

modules/Reducer.elm
subscriptions model =
    increase <| always Increase

ElmでのStateにあたるModelの型エイリアスを定義します。

modules/Reducer.elm
type alias Model =
    Int

ElmでのReducerにあたるupdateを定義します。

modules/Reducer.elm
update msg model =
    case msg of
        Increase ->
            ( model + 1, Cmd.none )

Elmアプリケーションの初期化関数initを定義します。

modules/Reducer.elm
init =
    ( 0, Cmd.none )

ModelをJSでの型に変換する関数encodeを定義します。

modules/Reducer.elm
encode model =
    int model

Elmランタイムにアプリケーションを登録します。

modules/Reducer.elm
main =
    Redux.program
        { init = init
        , update = update
        , encode = encode
        , subscriptions = subscriptions
        }

こうして作ったReducer.elmを、redux-elm-middlewareのcreateElmMiddlewareとcreateElmReducerを使ってJS側でReducerとして扱います。

configureStore.js
// @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をこちらに置いてありますので、興味があればごらんください。

また、もう少し本格的なアプリケーション開発に使えそうなボイラープレートも作っていますので、よかったらこちらもみてみてください。

react-redux-elm-boilerplate

もっとElmを知りたいと思ったら

この辺りを見るといいかもしれません。

17
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?