7.6

Retrieving App State from Local Storage

In this section, we’ll retrieve the posts saved in browser’s local storage and initialize the Elm app with them. Let’s start with JavaScript this time. Add the code for retrieving the state right above the line that finds the div whose id is elm-code-is-loaded-here 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 element = document.getElementById("elm-code-is-loaded-here");
    .
    .
</script>

The localStorage.getItem method lets us retrieve the state which is stored as raw JSON string. That method expects a key. post-app-save is the same key we used for saving the state in the previous section.

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 method. . How do we pass startingState to the Elm app? We can actually give it to Elm.App.embed as the second argument. Update that line in beginning-elm/index.html like this:

<script>
    .
    .
    var element = document.getElementById("elm-code-is-loaded-here");
    var app = Elm.App.embed(element, startingState);
    .
    .
</script>

startingState comes out on the Elm side as a flag. If we want to pass values to an Elm app during initialization we need to send them as flags. Flags are different from an incoming port which creates a subscription. Flags on the other hand are delivered directly to the initialization function which gets passed to the init field in main.

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

Like an incoming port, the JavaScript values passed as flags are automatically decoded by the runtime into the corresponding Elm values. Let’s modify the init function in PostApp/State.elm so that it can accept the flags too.

init : Maybe (List Post) -> Location -> ( Model, Cmd Msg )
init flags location =
    let
        currentRoute =
            Routing.extractRoute location

        posts =
            case flags of
                Just listOfPosts ->
                    RemoteData.succeed listOfPosts

                Nothing ->
                    RemoteData.Loading
    in
        ( initialModel posts currentRoute, fetchPostsCommand )

Flags are passed as the first argument to the init function. Their type varies between applications. In our case, it’s Maybe (List Post). 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>

In the let area of init, we attempt to extract the posts from flags. If they do exist, we need to use the succeed function defined in RemoteData module to convert the List Post type to RemoteData. succeed lifts an ordinary Elm value into the realm of RemoteData. Here’s how its type signature looks:

succeed : a -> RemoteData e a

If no posts are found, we need to initialize the posts field with RemoteData.Loading. Next modify the initialModel function in PostApp/State.elm so that it can accept posts.

initialModel : WebData (List Post) -> Route -> Model
initialModel posts route =
    { posts = posts
    , currentRoute = route
    , newPost = emptyPost
    }

To initialize an app with flags, we need to switch from Navigation.program to Navigation.programWithFlags in PostApp/App.elm.

main : Program (Maybe (List Post)) Model Msg
main =
    Navigation.programWithFlags LocationChanged
        { init = init
        , view = view
        , update = updateWithStorage
        , subscriptions = \_ -> Sub.none
        }

The type of main also changed from Program Never Model Msg to Program (Maybe (List Post)) Model Msg. The Never type indicates the program doesn’t accept any flags. Now that we’re accepting flags, we need to replace that with Maybe (List Post).

Navigation.programWithFlags behaves very much like Navigation.program. The only additional thing it does is accept flags. The Html module also provides a function called programWithFlags which is meant to be used by apps that don’t implement routing.

Here’s how Navigation.programWithFlags and Navigation.program’s type signatures look:

Navigation.programWithFlags
    :  (Location -> msg)
    -> { init : flags -> Location -> (model, Cmd msg)
       , update : msg -> model -> (model, Cmd msg)
       , view : model -> Html msg
       , subscriptions : model -> Sub msg
       }
    -> Program flags model msg


Navigation.program
    :  (Location -> msg)
    -> { init : Location -> (model, Cmd msg)
       , update : msg -> model -> (model, Cmd msg)
       , view : model -> Html msg
       , subscriptions : model -> Sub msg
       }
    -> Program Never model msg

And here are the type signatures for Html.programWithFlags and Html.program:

Html.programWithFlags
    :  { init : flags -> (model, Cmd msg)
       , update : msg -> model -> (model, Cmd msg)
       , subscriptions : model -> Sub msg
       , view : model -> Html msg
       }
    -> Program flags model msg


Html.program
    :  { init : (model, Cmd msg)
       , update : msg -> model -> (model, Cmd msg)
       , subscriptions : model -> Sub msg
       , view : model -> Html msg
       }
    -> Program Never model msg

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

$ elm-live PostApp/App.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 on the page when the app is launched and the server is not running, we’ll definitely know that those posts are from local storage.

Go to PostApp/State.elm and replace fetchPostsCommand with Cmd.none inside the in area in init.

init : Maybe (List Post) -> Location -> ( Model, Cmd Msg )
init flags location =
    let
        .
        .
    in
        ( initialModel posts currentRoute, Cmd.none )

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.

Summary

In this section, we learned how to retrieve the state of our app stored in the browser’s local storage. We used the JavaScript method localStorage.getItem() to retrieve the state and sent it to the Elm app via flags. The Elm runtime decodes JavaScript values inside flags into corresponding Elm values before passing them to the init function.

Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close