6.6

Retrieving Data on Initialization

Sometimes we need to fetch 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 returning Cmd.none from init to indicate we don’t want to run any commands during initialization.

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

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 =
    Http.get
        { url = "http://localhost:5019/posts"
        , expect =
            list postDecoder
                |> Http.expectJson (RemoteData.fromResult >> 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 json-server from the beginning-elm directory in terminal using the following command if it’s not running already.

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

Now run elm reactor from the beginning-elm directory in a different terminal window and go to this URL in your browser: http://localhost:8000/src/DecodingJson.elm. Don’t click the Get data from server button yet. Just wait for a second 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 broke 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/src/DecodingJson.elm one more time and the loading text should be back.

Let’s rename the button 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 fetchPosts.

fetchPosts : Cmd Msg
fetchPosts =
    Http.get
        .
        .

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

-- Before

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


-- After

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

Now replace SendHttpRequest with FetchPosts in view and update, and replace DataReceived with PostsReceived in fetchPosts and update.

Summary

In this section, we learned how to run a command when an app is being initialized. Here is the entire code from DecodingJson.elm thus far:

module DecodingJson exposing (main)

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


type alias Post =
    { id : Int
    , title : String
    , authorName : String
    , authorUrl : String
    }


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 (buildErrorMessage 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 (String.fromInt post.id) ]
        , td []
            [ text post.title ]
        , td []
            [ a [ href post.authorUrl ] [ text post.authorName ] ]
        ]


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


postDecoder : Decoder Post
postDecoder =
    Decode.succeed Post
        |> required "id" int
        |> required "title" string
        |> required "authorName" string
        |> required "authorUrl" string


fetchPosts : Cmd Msg
fetchPosts =
    Http.get
        { url = "http://localhost:5019/posts"
        , expect =
            list postDecoder
                |> Http.expectJson (RemoteData.fromResult >> PostsReceived)
        }


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

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


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

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

        Http.NetworkError ->
            "Unable to reach server."

        Http.BadStatus statusCode ->
            "Request failed with status code: " ++ String.fromInt statusCode

        Http.BadBody message ->
            message


init : () -> ( Model, Cmd Msg )
init _ =
    ( { posts = RemoteData.Loading }, fetchPosts )


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }
Back to top
Close