8.7

Retrieving App State from Local Storage

In this section, we’ll retrieve the app state saved in browser’s local storage and initialize our Elm app with it.

Retrieving App State

Let’s start with JavaScript this time. Add the code for retrieving posts right above the line that creates the app variable in beginning-elm/index.html.

<script>
    var storedState = localStorage.getItem('post-app-save');
    console.log("Retrieved state: ", storedState);
    var startingState = storedState ? JSON.parse(storedState) : null;

    var app = Elm.Main.init();
    .
    .
</script>

The localStorage.getItem function retrieves our app’s state which is stored in raw JSON string format. post-app-save is the same key we used for saving the state.

After retrieving the sate, we want to print it in the browser console so that we can see what it looks like. If the state does exist, the raw JSON string is parsed into JavaScript values using the JSON.parse function.

Sending App State to Elm via Flags

Next we need to pass startingState to our Elm app during initialization. How do we do that? By using flags. Update the line that initializes our Elm app in beginning-elm/index.html like this:

<script>
    .
    .
    var app = Elm.Main.init({flags: startingState});
    .
    .
</script>

Flags are different from an incoming port which creates a subscription. Flags on the other hand are delivered directly to the initialization function assigned to the init field in main.

main : Program () Model Msg
main =
    Browser.application
        { init = init
        .
        .

Just like an incoming port, the JavaScript values passed as flags are automatically decoded by the runtime into corresponding Elm values. Let’s change the type of flags in post-app/Main.elm from () to Maybe String.

init : Maybe String -> Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url navKey =
    ...

Flags are always passed as the first argument to the init function. Their type varies between applications. In our case, it’s Maybe String. It is possible that the app state might not have been saved in local storage. In that case, JavaScript will send null. To accommodate that, we need to use the Maybe type. Remember, null in JavaScript gets translated to Nothing in Elm.

<script>
    .
    .
    var startingState = storedState ? JSON.parse(storedState) : null;
    .
    .
</script>

We also need to replace () with Maybe String in the main function’s type annotation.

main : Program (Maybe String) Model Msg
main =
    ...

As discussed in the main Function’s Type Annotation section from chapter 5, by using () — unit type — we were letting the compiler know that no values were passed to our app during initialization. But since we are actually sending flags from JavaScript now we need to reflect that in main’s type annotation.

Decoding Stored Posts JSON

Now we need to decode the JSON string into a list of Post. Let’s add a function called decodeStoredPosts below init in Main.elm.

decodeStoredPosts : String -> WebData (List Post)
decodeStoredPosts postsJson =
    case decodeString postsDecoder postsJson of
        Ok posts ->
            RemoteData.succeed posts

        Err _ ->
            RemoteData.Loading

We’re using postsDecoder from Post.elm to decode JSON. decodeString produces a Result. If the process of decoding is successful we need to wrap posts in WebData type by calling the RemoteData.succeed function. Otherwise, we simply return the loading status. Later in ListPosts.elm, we’ll be fetching new posts right away if stored posts aren’t available and we need to display the Loading... text while the request is in flight.

Note: If you don’t remember how decodeString works, you may want to review the Decoding JSON section from chapter 6.

We need to import the Json.Decode, Post, and RemoteData modules in Main.elm.

module Main exposing (main)

import Json.Decode as Decode exposing (decodeString)
import Post exposing (Post, postsDecoder)
import RemoteData exposing (WebData)
.
.

Now let’s use decodeStoredPosts in Main.init.

init : Maybe String -> Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url navKey =
    let
        model =
            ...

        posts =
            case flags of
                Just postsJson ->
                    decodeStoredPosts postsJson

                Nothing ->
                    RemoteData.Loading
    in
    initCurrentPage posts ( model, Cmd.none )

Passing Stored Posts to ListPosts Page

Next we need to pass the stored posts to ListPosts.init inside initCurrentPage in Main.elm.

initCurrentPage :
    WebData (List Post)
    -> ( Model, Cmd Msg )
    -> ( Model, Cmd Msg )
initCurrentPage posts ( model, existingCmds ) =
    let
        ( currentPage, mappedPageCmds ) =
            case model.route of
                .
                .
                Route.Posts ->
                    let
                        ( pageModel, pageCmds ) =
                            ListPosts.init posts
                    in
                    ( ListPage pageModel, Cmd.map ListPageMsg pageCmds )
                .
                .
    in
    ...

Currently ListPosts.init doesn’t take posts as an input. Let’s modify it in ListPosts.elm.

init : WebData (List Post) -> ( Model, Cmd Msg )
init posts =
    ( initialModel posts, fetchPosts )


initialModel : WebData (List Post) -> Model
initialModel posts =
    { posts = posts
    , deleteError = Nothing
    }

Next we need to pass RemoteData.Loading as stored posts to initCurrentPage inside the ( UrlChanged url, _ ) -> branch in Main.update

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case ( msg, model.page ) of
        .
        .
        ( UrlChanged url, _ ) ->
            let
                ...
            in
            ( { model | route = newRoute }, Cmd.none )
                |> initCurrentPage RemoteData.Loading
        .
        .

Testing

We are now ready to test. Run the following command from the beginning-elm directory in terminal to compile the app.

$ elm-live post-app/Main.elm --pushstate -- --output=elm.js

You shouldn’t see any errors. Also start json-server by running the following command from the beginning-elm directory in a separate terminal window.

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

Now go to http://localhost:8000/ in a browser and open the browser console. You should see the retrieved state printed as JSON in the console.

How do we verify that our app did get initialized with the stored state? Let’s stop json-server by pressing Ctrl + c and see how the app behaves when the server is unreachable. Refresh the page at http://localhost:8000/.

Unfortunately, we see an error instead of the posts retrieved from local storage. How about we don’t fetch the posts from a server when the app is initialized? That way if we see a list of posts when the app is launched and the server is not running, we’ll definitely know that those posts are from local storage. Update the init function in ListPosts.elm like so:

init : WebData (List Post) -> ( Model, Cmd Msg )
init posts =
    let
        initialCmd =
            if RemoteData.isSuccess posts then
                Cmd.none

            else
                fetchPosts
    in
    ( initialModel posts, initialCmd )

Now if you refresh the page at http://localhost:8000/, you should see the posts. Those are definitely loaded from local storage.

Bring json-sever back up by running the following command from the beginning-elm directory in terminal.

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

Click the Refresh posts button from the home page. If you open the browser console, you should see both the saved and retrieved states. Whenever we retrieve posts from the server, they immediately get saved in local storage.

Verifying Flags

Let’s see what happens if we try to pass a non-string value to Elm using flags. Inside beginner-elm/index.html, replace startingState with 10 in line that initializes our Elm app.

<script>
    .
    .
    var app = Elm.Main.init({flags: 10});
    .
    .
</script>

Now refresh the page at http://localhost:8000/. You should see the following error in browser console.

Our Main.init function is expecting flags to be of type Maybe String, but we sent a number instead. Flags must be exactly what the init function expects, otherwise Elm throws an error on JavaScript side. Without this check, we could pass anything leading to runtime errors in Elm.

It’d be better to show a user friendly error message instead of crashing our app like this. To do that, we need to convert the JavaScript values to Elm values ourselves by changing the flags’ type from Maybe String to Value in Main.init.

init : Value -> Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url navKey =
    ...

We also need to change Maybe String to Value in the main function’s type annotation.

main : Program Value Model Msg
main =
    ...

As mentioned in Protecting Boundaries between Elm and JavaScript, the Value type represents a JSON value. To decode a JSON value ourselves, we need to use the decodeValue function. Replace the logic for computing posts in let area of Main.init with the following.

init : Value -> Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url navKey =
    let
        .
        .
        posts =
            case decodeValue Decode.string flags of
                Ok postsJson ->
                    decodeStoredPosts postsJson

                Err _ ->
                    Http.BadBody "Flags must be either string or null"
                        |> RemoteData.Failure
    in
    ...

If a flag of invalid type is sent from JavaScript, we need to create an error. The WebData type’s definition indicates that RemoteData.Failure requires the error to be of type Http.Error. That’s why we’re using the Http.BadBody data constructor to build an error.

type alias WebData a =
    RemoteData Http.Error a


type RemoteData e a
    = NotAsked
    | Loading
    | Failure e
    | Success a

Let’s import the Http module and expose Value and decodeValue from Json.Decode in Main.elm.

module Main exposing (main)

import Json.Decode as Decode exposing (Value, decodeString, decodeValue)
import Http
.
.

Stop json-server and refresh the page at http://localhost:8000/ and you shouldn’t see the decoding error in console anymore. Unfortunately, the error we see on the page says Unable to reach server instead of Flags must be either string or null. That’s because in ListPosts.init we’re firing the command for fetching posts unless the posts parameter is RemoteData.Success.

init : WebData (List Post) -> ( Model, Cmd Msg )
init posts =
    let
        initialCmd =
            if RemoteData.isSuccess posts then
                Cmd.none

            else
                fetchPosts
    in
    ( initialModel posts, initialCmd )

Let’s also not fire the fetchPosts command when the posts parameter is RemoteData.Failure by updating the if expression to the following.

init : WebData (List Post) -> ( Model, Cmd Msg )
init posts =
    let
        initialCmd =
            if RemoteData.isSuccess posts || RemoteData.isFailure posts then
                Cmd.none

            else
                fetchPosts
    in
    ( initialModel posts, initialCmd )

Refresh the page at http://localhost:8000/ one more time and you should see the Flags must be either string or null error.

Summary

In this section, we learned how to retrieve the state of our app stored in browser’s local storage. We used the JavaScript function localStorage.getItem() to retrieve the state and sent it to our Elm app via flags. Elm runtime automatically decodes JavaScript values inside flags into corresponding Elm values before passing them to the init function. A more robust approach is to receive flags as Value and decode it ourselves.

Back to top
Close