非同期通信を発生させるコンポーネント
さて、ここからが本題です。TaskをElm Architectureに組み込むにはどうすればよいでしょう。 先ほどの例では、初期化時に1回だけ、データ取得のために通信していましたが、ユーザアクションを引き金にして通信が発生するケースはよくあるでしょう。 ここでは、ボタンを押したときにサーバからデータを取ってくるようなケースを考えます。
いきなり答えを出す前に、まずは今まで使ってきた道具を用いて実現できないかを考えてみます。 アクションを引き金にしてTaskが発生するので、update関数で処理するのが適切でしょう。 そこで、update関数がTaskを返すようにします。すると、次のようになります。
update : Address Action -> Action -> Model -> (Model, Maybe (Task x Action))
update address action model =
case action of
Load ->
let
task = getData `andThen` (\data -> Signal.send address (Data data))
in
(model, Just task) -- Taskがある場合
Data data ->
({ model |
data <- data
}, Nothing) -- Taskがない場合
Loadというアクションを引き金にしてデータを非同期で取得し、その結果を今度はDataというアクションとして受け取るようにしています。 結果がタプルになるのは仕方ないとしても、Addressが登場するなど、少々複雑になってしまいました。
Effectsの導入
Taskの負担を和らげるために、最近、Elmに「Effects」というモジュールが導入されました。 Effectsは「evancz/elm-effects」パッケージで提供されています。
先ほどのコードをEffectsを使って書き直すと、次のようになります。
update : Action -> Model -> (Model, Effects Action)
update action model =
case action of
Load ->
(model, Effects.task (Task.map Data getData)) -- TaskをEffectsに変換
Data data ->
({ model |
data <- data
}, Effects.none) -- Effectsがない場合
Addressがなくなってスッキリしました。 Elm ArchitectureではAddress(を提供するMailbox)はシングルトンであるため、Addressを指定する手間を省略しているのです。 EffectsはAddressを後から与えられることで、Taskに還元されます。
toTask : Address (List a) -> Effects a -> Task Never ()
先ほど少し触れた「start-app」ライブラリを使うと、この変換処理は完全にライブラリに預けることができます。
サンプル:押すとデータを取得するボタン
以下は、「ボタンを押すと、サーバから取得したデータがボタン自身に表示される」サンプルの全ソースです。update関数の定義が変わっていることと、「start-app」内でEffectsからTaskへの変換が行われ、それをportしているところに注目してください。
ここでは簡単にするために、ボタンの数を1つに戻し、マウスオーバーの処理も除いています。完全版は本記事のサンプル(sample.zip)をダウンロードしてください。
module Counter where
import Html exposing (..)
import Html.Attributes exposing (style)
import Html.Events exposing (onClick)
import Task exposing (Task, onError)
import Effects exposing (Effects)
-- MODEL
type alias Model =
{ data : String
}
init : Model
init =
{ data = "(click me!)"
}
-- UPDATE
type Action
= Load
| Data String
update : Action -> Model -> (Model, Effects Action)
update action model =
case action of
Load ->
-- データを取得し、コールバックをDataアクションとして発行する
(model, Effects.task (Task.map Data getData))
Data data ->
({ model |
data <- data
}, Effects.none)
getData : Task x String
getData = Task.succeed "This is the data from the server"
-- VIEW
view : Signal.Address Action -> Model -> Html
view address model =
button
[ onClick address Load -- クリック時にLoadアクションを発行
, style
[ ("width", "100px")
, ("height", "100px")
, ("font-size", "large")
]
]
[ text model.data ]
import Html exposing (..)
import Html.Attributes exposing (style)
import Counter
import Task exposing (Task)
import Effects exposing (Effects, Never)
import StartApp -- Effectsに対応した(Simpleではない方の)StartAppを使う
app =
StartApp.start
{ init = init
, update = update
, view = view
, inputs = []
}
main =
app.html
-- StartAppがEffectsをTaskのSignalに変換してくれるので、動作させるためにportする
port tasks : Signal (Task.Task Never ())
port tasks =
app.tasks
-- MODEL
type alias Model =
{ title : String
, counter : Counter.Model
}
init : (Model, Effects Action)
init =
({ title = "Simple Data Loader"
, counter = Counter.init
}, Effects.none) -- 最初のEffectsはなし
-- UPDATE
type Action
= NoOp
| CounterAction Counter.Action
update : Action -> Model -> (Model, Effects Action)
update action model =
case action of
NoOp -> (model, Effects.none)
CounterAction action ->
let
-- update関数の結果をタプルで受け取り、展開する
(newCounter, eff) =
Counter.update action model.counter
in
({ model |
counter <- newCounter
}, Effects.map CounterAction eff) -- ActionをCounterActionでラップ
-- VIEW
view : Signal.Address Action -> Model -> Html
view address model =
div
[]
[ h1 [] [text model.title]
, Counter.view (Signal.forwardTo address CounterAction) model.counter
]
ここでもやはりActionは階層化され、コンポーネントの発行したActionは外部からは詳細を知らずに対応できるようになっています。
* * *
全2回にわたってElmの仕組みとアプリケーションの作り方について解説しました。 ここまでで、基本的なアプリケーションならば一通り作れるようになっています。 より現実的な問題(例えばJavaScriptライブラリやブラウザAPIとの連携など)については説明を省いているため、公式サイトや他のElmに関する記事を参考にしてください。
Elmはまだまだ進化を続けています。多少こなれていない機能も今後改善していくでしょう。 Webフロントエンド開発の新しい選択肢の1つとして、ぜひ検討してみてください。
