まとめ
type Status a
= NotAsked
| Loading
| Failure Http.Error
| Success a
こういう型を作っておくといい感じにAPIから取ってくる値を扱えて便利です
はじめに
ElmのHttpはエラーハンドリングをさぼれないのが、いきなりやるのは難しいですよね(もちろんエラーハンドリングはさぼれるんですがどこでどうやろうって困惑すると思います)
JavaScriptだったら正常系をとりあえず書いておけばいいですからね
今回はエラーを含めた状態を型で表してこの問題に対処したいと思います
型
良いこのみんなはelm guideやpackageサイトをちゃんとよく読んでると思うので知ってると思うのですがネタ元は以下です
elm guideを引用します
type Profile = Failure | Loading | Success { name : String, description : String }
Profile のデータの状態をLoadingから始めて、フェッチに失敗したらFailure、成功したらSuccessというように何が起こったかに応じて状態を遷移させることができます。このような設計はview関数を書くのを本当にシンプルにします。データの状態がカスタム型で表現されているのでview関数はデータをロードしているときも常に妥当な見た目を表示することができます。
もう何もいうことはないです(ちなみにここの翻訳はわたしです)
自分だけの型を設計する
package化されたkrisajenkins/remotedataもありますが結局自分で作った方がいいと思います
エラーをどう扱うかって段階的に変わっていきますから
わたしが今触ってるコードでは以下のように認証エラーだけ特別扱いしています。リトライ処理なんかに使われます
Error型を工夫すれば汎用型でも処理できる範囲なんですが、型はできるだけ平たいほうが扱いやすいです。型がネストしてると面倒です
type Status a
= NotAsked
| Loading
| Failure Http.Error
| AuthFailure
| Success a
型はできるだけ平たいほうが扱いやすいといいつつHttp.Errorがあるということは握りつぶしていることを示しています。ご容赦ください
Status
という命名はネタ元を覚えてないです。RemoteDataでもなんでもいいです
Result Http.Error a
から変換する
elm/httpのexpect系の関数をみてみましょう
expectJson : (Result Error a -> msg) -> Decoder a -> Expect msg
Result Error a -> msg
ここの部分は以下のようなMsgに直接食べさせてるんじゃないかと思います
type Msg = GotItem (Result Http.Error Item)
これを
type Msg = GotItem (Status Item)
-- Status.elm
fromHttpResult : Result Http.Error a -> Status a
fromHttpResult result =
case result of
Ok data ->
Success data
Err err ->
Failure err
こうします
fromHttpResultをexpectで使ってupdateにはStatus aが来るだけにします。(見た目だけ)Resultがなくなってすっきりしましたね!
あとはこのStatus aの値をModelにいれて、viewで表示するだけです
まとめ1
type Status a
= NotAsked
| Loading
| Failure Http.Error
| Success a
APIの状態を含めて型に落とし込むと便利
データをキャッシュしたい
ところでこのStatus型は新しくデータを取りに行くとLoading
になって前取ってきた値がなくなってしまいますね
問題ないってときもあれば、新しい値をとってこれるまでは昔の値を表示しておきたいときもあると思います
type alias WithStatus a =
{ status : Status ()
, data : a
}
ということでこういう型を使ってます
a型は大体ListやDictになります。
使い方
例えば本の情報を取得するサービスに
- IDを指定して一冊本の情報を取得
- ジャンルを指定して複数冊本の情報を取得
2つのAPIがあるとします
type alias Book = {..}
type alias Books = List Book
type alias Model = { books : WithStatus Books }
こんな感じでModelにいれてます
-- 複数冊取得
GotBooks status ->
{ model | books = Status.updateWithStatus Books.update status model.books }
-- 1冊取得
GotBook status ->
{ model | books = Status.updateWithStatus Books.updateByBook status model.books }
-- Books.elm
update : Books -> Books -> Books
update new old =
...
updateByBook : Book -> Books -> Books
updateByBook new old =
...
こんな感じで更新します
あとはviewでStatusを表示しつつ保持しているコレクションも表示すればいいです
まとめ2
わたしが使っている型についての話でした
type alias WithStatus a =
{ status : Status ()
, data : a
}
WithStatus型でAPIから取得する値をうまいこと扱おう