LiveView: View state recovery

There has been discussion in various threads around how to handle cases where the user might be in the middle of some work at a URI that uses LiveView when a deployment occurs. This causes the LiveView process to restart and potentially, the user to lose their work.

In my case, I have a fairly complex view that allows the user to ‘select’ various elements with the selections stored in the LV state. On a deployment, this state is lost causing the LV to review to the blank state.

The docs offer some guidance for state recovery that focuses on forms, using the pxh-auto-recover binding:

On the server, the "validate_wizard_step" event is only concerned with the current client form data, but the server maintains the entire state of the wizard. To recover in this scenario, you can specify a recovery event, such as "recover_wizard" above, which would wire up to the following server callbacks in your LiveView

I assume this only works for forms. My question is, what about other cases of state? Can I use that binding on non form elements to recover other state? And how is the server expected to “maintain” the state (of the form or anything else)? In some sort of persistent storage it can read from even after a restart?


1 Like

Anything non persistent will be gone after restart, so yes you need a persistent storage.

Why would you think so? Any data your liveview process knows about can be persisted – even if just using :erlang.term_to_binary/:erlang.binary_to_term. You could use other means of serialization into your datastore, those might not support any erlang term.

How exactly you architect a solution here depends heavy on your problem though. You could use ecto/dets/mnesia or many other persistent data storages and each come with certain tradeoffs you have to decide on.

I was just extrapolating from the examples and my experience with other bindings that are form specific (like phx-change). I should have tested that assumption before posting here though. I’ll give it a shot now.

I can’t seem to trigger the recover event. Here’s what I tried:

  1. Set code_reloader: false on my endpoint config, as per documentation.

  2. Added phx-auto-recover="recover" in the outermost div of my LV template.

  3. Added the following to my LV module:

      def handle_event("recover", _params, socket) do
        {:noreply, socket}

After that I tried aborting and then restarting the server process. After the abort, I saw the socket attempt to reconnect, adding the loading class as expected, but after I restarted it loaded the blank state and I did not observe any warning text in the logs.

Any idea what I’m doing wrong?

I managed to work out a solution for this without using LiveView’s built in form auto-recovery feature, and it turned out to be pretty straightforward. Here are the steps in case anyone else is looking to do something similar:

  1. After playing around a bit with mnesia, I decided to use @lucaong’s CubDB instead because I just needed a very simple persistent KV store. CubDB has been serving that purpose admirably so kudos and thanks to @lucaong.

  2. Added CubDB db to my app’s supervisor:

       {CubDB, data_dir: Application.get_env(:my_app, :cubdb_data_dir), name: MyApp.RecoveryCache}
  3. Added a binding for my “selection” event and a handler in my LV module that stores the resulting state:

     CubDB.put(MyApp.RecoveryCache, {:selections, user_id:}, selections)
  4. Added some conditional code in the mount handler that checks for existing cache and adds it to the LV assigns:

    socket =
     if connected?(socket) do
       selections =
           {:selections, user_id:}
       socket |> assign(selections: selections)

That’s it! Now after making some selections on the page and either deploying the app, restarting the server, or just reloading the page, the previous selections are magically still there! Amazing.


Another thing to keep in mind I haven’t seen mention in this context (maybe because it’s so obvious) is to consider whether the state in question is simple enough to store in the url, and makes sense there. Because with push_patch and handle_params you can get state restoration for basically free that way.

I was most of the way through persisting a simple index to disk before I thought of this… :expressionless: