6.6

Retrieving Data on Initialization

Sometimes we need to retrieve data from a server when an app is being initialized. In this section, we will learn how to do just that. So far in this chapter, we have been passing Cmd.none to the init property in main to indicate that we don’t want to run any commands during initialization.

init : ( Model, Cmd Msg )
init =
    ( { posts = RemoteData.NotAsked }, Cmd.none )


main : Program Never Model Msg
main =
    program
        { init = init
        .
        .

Let’s change that. We will be iterating on the same app we built in the last few sections. The good news is we have already extracted the logic for creating a command out to httpCommand in DecodingJson.elm.

httpCommand : Cmd Msg
httpCommand =
    list postDecoder
        |> Http.get "http://localhost:5019/posts"
        |> RemoteData.sendRequest
        |> Cmd.map DataReceived

All that is left to do is replace Cmd.none with httpCommand in init.

init : ( Model, Cmd Msg )
init =
    ( { posts = RemoteData.NotAsked }, httpCommand )

Run elm-reactor from the beginning-elm directory in terminal if it’s not running already and go to this URL in your browser: http://localhost:8000/elm-examples/DecodingJson.elm. Don’t click the Get data from server button yet. Just wait for two seconds and you should see the posts.

By passing a command to init, we’re telling the Elm Runtime to fetch posts when the app is being initialized. But we missed something along the way. The text Loading... has disappeared again. That’s because we’re initializing the posts field in our model with NotAsked instead of Loading. Let’s fix that.

init : ( Model, Cmd Msg )
init =
    ( { posts = RemoteData.Loading }, httpCommand )

Refresh the page at http://localhost:8000/elm-examples/DecodingJson.elm one more time and the loading text should be back.

If you still don’t see the loading text, make sure the json-server command is run with the --delay option.

$ json-server --watch server/db.json -p 5019 --delay 2000

Click the Get data from server button to make sure that it’s still working. Let’s rename that button’s title to Refresh posts to reflect its new purpose.

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick SendHttpRequest ]
            [ text "Refresh posts" ]
        , viewPostsOrError model
        ]

While we are at it, let’s also rename httpCommand to fetchPostsCommand.

fetchPostsCommand : Cmd Msg
fetchPostsCommand =
    list postDecoder
        |> Http.get "http://localhost:5019/posts"
        |> RemoteData.sendRequest
        |> Cmd.map DataReceived

Don’t forget to replace httpCommand with fetchPostsCommand in init and update functions. We should also rename the messages.

-- Before

type Msg
    = SendHttpRequest
    | DataReceived (WebData (List Post))


-- After

type Msg
    = FetchPosts
    | PostsReceived (WebData (List Post))

Now replace the old message names. SendHttpRequest should now be FetchPosts in the view and update functions, and DataReceived should now be PostsReceived in the fetchPostsCommand and update functions.

Summary

In this section, we learned how to run a command when an app is being initialized. In the next section, we’ll reorganize our code by splitting it into smaller modules. Here is the entire code from DecodingJson.elm thus far:

module DecodingJson exposing (..)

import Html exposing (..)
import Html.Attributes exposing (href)
import Html.Events exposing (onClick)
import Http
import Json.Decode exposing (string, int, list, Decoder)
import Json.Decode.Pipeline exposing (decode, required)
import RemoteData exposing (WebData)


type alias Author =
    { name : String
    , url : String
    }


type alias Post =
    { id : Int
    , title : String
    , author : Author
    }


type alias Model =
    { posts : WebData (List Post)
    }


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick FetchPosts ]
            [ text "Refresh posts" ]
        , viewPostsOrError model
        ]


viewPostsOrError : Model -> Html Msg
viewPostsOrError model =
    case model.posts of
        RemoteData.NotAsked ->
            text ""

        RemoteData.Loading ->
            h3 [] [ text "Loading..." ]

        RemoteData.Success posts ->
            viewPosts posts

        RemoteData.Failure httpError ->
            viewError (createErrorMessage httpError)


viewError : String -> Html Msg
viewError errorMessage =
    let
        errorHeading =
            "Couldn't fetch data at this time."
    in
        div []
            [ h3 [] [ text errorHeading ]
            , text ("Error: " ++ errorMessage)
            ]


viewPosts : List Post -> Html Msg
viewPosts posts =
    div []
        [ h3 [] [ text "Posts" ]
        , table []
            ([ viewTableHeader ] ++ List.map viewPost posts)
        ]


viewTableHeader : Html Msg
viewTableHeader =
    tr []
        [ th []
            [ text "ID" ]
        , th []
            [ text "Title" ]
        , th []
            [ text "Author" ]
        ]


viewPost : Post -> Html Msg
viewPost post =
    tr []
        [ td []
            [ text (toString post.id) ]
        , td []
            [ text post.title ]
        , td []
            [ a [ href post.author.url ] [ text post.author.name ] ]
        ]


type Msg
    = FetchPosts
    | PostsReceived (WebData (List Post))


authorDecoder : Decoder Author
authorDecoder =
    decode Author
        |> required "name" string
        |> required "url" string


postDecoder : Decoder Post
postDecoder =
    decode Post
        |> required "id" int
        |> required "title" string
        |> required "author" authorDecoder


fetchPostsCommand : Cmd Msg
fetchPostsCommand =
    list postDecoder
        |> Http.get "http://localhost:5019/posts"
        |> RemoteData.sendRequest
        |> Cmd.map PostsReceived


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchPosts ->
            ( { model | posts = RemoteData.Loading }, fetchPostsCommand )

        PostsReceived response ->
            ( { model | posts = response }, Cmd.none )


createErrorMessage : Http.Error -> String
createErrorMessage httpError =
    case httpError of
        Http.BadUrl message ->
            message

        Http.Timeout ->
            "Server is taking too long to respond. Please try again later."

        Http.NetworkError ->
            "It appears you don't have an Internet connection right now."

        Http.BadStatus response ->
            response.status.message

        Http.BadPayload message response ->
            message


init : ( Model, Cmd Msg )
init =
    ( { posts = RemoteData.Loading }, fetchPostsCommand )


main : Program Never Model Msg
main =
    program
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }
Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close