Saving App State in Local Storage

In chapter 6 we built an app that allowed us to create, read, update, and delete posts from an HTTP server. What happens to the app when the server is unreachable? Let’s find out. Run the following command from the beginning-elm directory in terminal to launch the app.

$ elm-live PostApp/App.elm --pushstate

Also start an HTTP 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. You should see two posts.

Let’s stop json-server by pressing Ctrl + c. After that reload the page at http://localhost:8000/. You should now see an error.

It would be great if we could display the posts from the previous fetch as soon as the app loads even if the server is unreachable. As it turns out, all client-side web applications can in fact store data locally in a browser through the use of a technology called Web Storage which provides two different ways to save data:

  • Session storage - Data stored in session storage is available until the browser is closed.

  • Local storage - Data stored in local storage is available even if the browser is closed and reopened.

Let’s store the state of our app in local storage. Unfortunately, Elm doesn’t provide a package for storing data in local storage. We’ll have to use the JavaScript API for that.

The Elm core team is working hard to expand support for all technologies that are part of the Web Platform. Local storage is one of them. In the meantime, the recommended approach is to use the JavaScript APIs through ports.

Saving State in Local Storage

Let’s start by defining an outgoing port for sending the app state to JavaScript. Create a new file called Ports.elm in the beginning-elm/PostApp directory and add the code below to it.

port module Ports exposing (..)

import Types exposing (..)

port storePosts : List Post -> Cmd msg

We have created an outgoing port function called storePosts which takes a list of posts as an input and returns a command. In the Model View Update - Part 1 section, we learned that the state of an app is typically represented by a model. So why are we storing only the posts and not everything inside our model?

type alias Model =
    { posts : WebData (List Post)
    , currentRoute : Route
    , newPost : Post

Not everything inside the model is needed to bring the app back to a usable state. In our case, all we need is a list of posts. That’s why we aren’t storing the currentRoute and newPost fields.

The posts field in our model actually contains the WebData type. We need to somehow extract the actual posts stored inside WebData. We’ll do that in a new function called updateWithStorage. Let’s add it right above the update function in PostApp/State.elm.

updateWithStorage : Msg -> Model -> ( Model, Cmd Msg )
updateWithStorage msg model =
        ( newModel, commands ) =
            update msg model

        extractedPosts =
            RemoteData.toMaybe newModel.posts
                |> Maybe.withDefault []
        ( newModel
        , Cmd.batch [ commands, storePosts extractedPosts ]

We’re doing more than just extracting posts in the above function. Our strategy here is to have the Elm runtime send all messages to updateWithStorage instead of update. Why do we want to do that?

Whenever the update function returns a new model, we want to extract the posts out and save them in local storage. Rather than creating a command for doing that in every single branch of update, we created a wrapper function that uses Cmd.batch to append the command for storing posts to the list returned by update.

Let’s go over the code that extracts posts from the new model real quick to make sure that you understand how it works.

extractedPosts =
    RemoteData.toMaybe newModel.posts
        |> Maybe.withDefault []

What we’re doing here is convert the WebData type stored in the posts field to a Maybe. After that we can use the withDefault function to pull the posts out. The posts are actually stored as a payload inside the Success data constructor.

type RemoteData error value
    = NotAsked
    | Loading
    | Failure error
    | Success value

If you don’t remember how the WebData type works, you might want to read the RemoteData section from chapter 6 once again.

If the value inside the posts field is anything other than Success, the RemoteData.toMaybe returns Nothing in which case we assign an empty list to the extractedPosts constant.

We need to import the Ports module in State.elm. That’s where the storePosts outgoing port function is defined.

module State exposing (..)
import Misc exposing (..)
import Ports exposing (..)

Now let’s replace update with updateStorage inside main in App.elm.

main : Program Never Model Msg
main =
    Navigation.program LocationChanged
        { init = init
        , view = view
        , update = updateWithStorage
        , subscriptions = \_ -> Sub.none

We also need to expose updateWithStorage in App.elm.

module App exposing (main)

import Html exposing (program)
import State exposing (init, updateWithStorage)

That’s all we need to do on the Elm side. Check the elm-live window in terminal to make sure there are no errors.

Compiling Our Code to elm.js

Let’s turn our attention to JavaScript now. Create a new file called index.html in the beginning-elm directory if it doesn’t exist already. and replace its contents with the code below.

<!DOCTYPE html>
    <div id="elm-code-is-loaded-here"></div>
    <script src="elm.js"></script>
        var element = document.getElementById("elm-code-is-loaded-here");
        var app = Elm.App.embed(element);

All we’re doing here is load the elm.js file and embed the Elm app in a div. Next we need to put the compiled Elm code in elm.js. Stop elm-live if it’s running already by pressing Ctrl + c and restart it by running the following command from the beginning-elm directory in terminal.

$ elm-live PostApp/App.elm --pushstate --output=elm.js

The --output option tells elm-live to compile the code in App.elm and put it in elm.js. If we don’t use the --output option, elm-live will automatically create a file called index.html in the directory from where it’s run and put the compiled code in that file instead.

In the previous sections of this chapter we used elm-make to produce elm.js like this:

$ elm-make Ports/PortExamples.elm --output Ports/elm.js

Why couldn’t we use elm-make here too? That’s because PostApp implements routing and we need the pushstate feature from elm-live to be able to navigate between pages.

Listening to storePosts Port

Replace the code inside the last <script> tag in beginning-elm/index.html with the following code.

    var element = document.getElementById("elm-code-is-loaded-here");
    var app = Elm.App.embed(element);

    app.ports.storePosts.subscribe(function(posts) {
        console.log("Posts: ", JSON.stringify(posts));

We’re now listening to the storePosts outgoing port defined in the Elm app.

port storePosts : List Post -> Cmd msg

For now, we’re simply printing the data coming through the port in the browser console. We need to first make sure that we’re actually receiving the posts sent by the Elm app before we save them in local storage.

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

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

Go to http://localhost:8000/ in a browser and open the browser console. You should see the posts in a raw JSON string format.

Now update the callback function in beginning-elm/index.html to actually save the posts in local storage.

    var element = document.getElementById("elm-code-is-loaded-here");
    var app = Elm.App.embed(element);

    app.ports.storePosts.subscribe(function(posts) {
        if (posts.length > 0) {
            var postsJson = JSON.stringify(posts);
            localStorage.setItem('post-app-save', postsJson);
            console.log("Saved state: ", postsJson);

We’re using the setItem method in localStorage to save the posts. That method takes a key value pair. post-app-save is the key and the raw JSON string produced by the JSON.stringify method is the value.

If you are using Chrome, you can check whether or not the data was indeed saved in local storage by opening the developer tools and going to the Application tab as shown in the figure below.

Fix Routing

Before we move on, we need to fix routing in PostApp. Right now if you click the Create new post link, you’ll be taken to http://localhost:8000/posts/new, but that page is blank.

If you open the browser console, you’ll see 404 (Not Found) error. You’ll also see the Elm is not defined error.

Where are these errors coming from? Let’s see what’s going on in the terminal.

Why is elm-live trying to load the path /posts/elm.js instead of /posts/new? The reason for that is elm-live needs to serve the entire app — contained in elm.js — regardless of which path is being loaded for routing to work properly. That’s why you see a redirect to /elm.js when we went to the home page whose path is /.

How come we didn’t receive an error when we went to the home page? That’s because elm-live served elm.js from the root directory.

And elm-live in fact did put the elm.js file in our root directory (beginning-elm) — the directory from which we ran the elm-live command earlier.

But when we try to go to the /posts/new path, instead of serving elm.js from the root directory again, elm-live decides to look for it in the /posts directory which doesn’t exist. If elm.js isn’t loaded then the Elm app can’t be initialized. That’s why that error Elm is not defined, we saw earlier, was displayed in the browser console.

How do we fix it? Believe it or not all we need to do is add a forward slash in front of elm.js in the script tag that loads it. Make that change in beginning-elm/index.html.

    <div id="elm-code-is-loaded-here"></div>
    <script src="/elm.js"></script>

Now elm-live will serve elm.js from the root directory no matter which path we go to. Now if you click the Create new post from http://localhost:8000/, you should see the contents of the page for creating a new post.


In this section, we learned how to save the state of our Elm app in browser’s local storage. Since Elm doesn’t provide a package for that, we had to send the app state through an outgoing port and use the JavaScript API on the other end to actually save the data. In the next section, we’ll learn how to restore the state when our app is being initialized.

Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required