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
}