Optimizing Phoenix LiveView State Restoration

Fly.io runs apps close to users, by transmuting Docker containers into micro-VMs that run on our own hardware around the world. This is a recipe about how you can optimize restoring Phoenix LiveView state that’s stored in the browser. If you are interested in deploying your own Elixir project, the easiest way to learn more is to [try it out](/content/docs/speedrun/ ""/index.html); you can be up and running in just minutes.

You are storing some LiveView state in the browser and want to retrieve that saved state as early as possible to improve the user experience. How can you do that?

Problem

The approach in Saving and Restoring LiveView State waits for the LiveView to request the client to send the data. Can we have it automatically pushed from the client without being requested? Can we do it in a reusable way across multiple pages and LiveViews?

Solution

When you know you want the data stored in the browser during the startup of your LiveView, there is a way to send it up when the socket connection is being set up. The data could be written into the HTML of the page or stored in sessionStorage or localStorage.

Here’s an example of the app.js file:

import "phoenix_html"
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

let csrfToken =
  document.querySelector("meta[name='csrf-token']")
  .getAttribute("content")
let liveSocket =
  new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})

topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide())

liveSocket.connect()
window.liveSocket = liveSocket

To handle the parameters:

let params = () => {
  return {_csrf_token: csrfToken, restore: sessionStorage.getItem(...) }
}

let liveSocket = new LiveSocket("/live", Socket, {params: params})

On the server, you can read the connect params in the mount callback:

  def mount(_params, _session, socket) do
    case get_connect_params(socket) do
      %{"restore" => token} -> ...

To avoid unnecessary data sending on each page load, we can enhance it with custom DOM attributes in our HTML:

<div data-state-restore="true" data-session-key="my_special_key">
  ...
</div>

Modify app.js to check for these attributes:

let params = (node) => {
  var restoreNode =
    node && node.querySelector("div[data-state-restore='true']")
  if (restoreNode) {
    var key = restoreNode.getAttribute("data-session-key")
    return {_csrf_token: csrfToken, restore: sessionStorage.getItem(key)}
  }
  else {
    return {_csrf_token: csrfToken}
  }
}

If your LiveView does not include data-state-restore, then no extra data is processed. If it is present, it uses the key to look up sessionStorage for any data.

Discussion

Phoenix and LiveView provide the necessary hooks for this feature to work efficiently, allowing for fast restoration of the LiveView state during the startup process. This improves the user experience by restoring state quickly and avoiding extra roundtrips for data requests.

Fly ❤️ Elixir

Fly is an awesome place to run your Elixir apps. It’s really easy to get started. You can be running in minutes.