<!-- LLM_VERSION_INFO
FORMAT: text/markdown
CONTENT_TYPE: article
ORIGINAL_URL: https://fly.io/phoenix-files/how-phoenix-liveview-form-auto-recovery-works
ALTERNATE_VERSION: phoenix-files/how-phoenix-liveview-form-auto-recovery-works/index.html (text/html)
EXTRACTION_DATE: 2026-04-18T22:17:48.021Z

This is the markdown version with text-only content (images converted to alt-text).
For rich formatting with images, request the HTML version at: phoenix-files/how-phoenix-liveview-form-auto-recovery-works/index.html
-->

**Author**  
**Jason Stiebs**  
[@peregrine](https://twitter.com/peregrine)

**Image by [Annie Ruygt](https://annieruygtillustration.com/)**

We’re Fly.io and we transmute containers into VMs, running them on our hardware around the world. We have fast booting VM’s, so why not [take advantage of them](/content/docs/speedrun/index.html)?

Phoenix LiveView is a server-side frontend framework and with this great power comes great responsibility on the developer. A very common source of frustration is around Forms, their assigns values and connection resets or deploys. In this post we’ll discuss how this all works and how it enables Form Auto-Recovery.

Let’s say we have this basic LiveView with a form and a `query` input:

```elixir

defmodule FormExamples.FormsLive do
  use FormExamplesWeb, :live_view

def mount(_params, _session, socket) do
    {:ok,
      socket
      |> assign(form: to_form(%{"query" => nil}))
    }
  end

def render(assigns) do
    ~H"""
    <.simple_form for={@form} phx-submit="save" id="form" class="mb-20">
      <.input field={@form[:query]} label="Search" />
    </.simple_form>
    """
  end

def handle_event("save", %{"query" => q}, socket) do
    {:noreply,
      socket
      |> assign(form: to_form(%{"query" => nil}))
      |> put_flash(:info, "Form submitted with query: #{q}")}
  end
end
```

When we type into the text box and hit enter, our `save` callback is called and we reset the form. Before we move on, can you see the problem with this form?

On our Form submit we are `assign`ing `"query" => nil` but `"hello"` remains in our text box. What’s going on here?

Spoiler: We did not change the `assign` value while we were typing the query, in LiveView’s view of the world the `form`‘s `query` value is nil, and it never changed. These implications will be discussed further in the article.

## The Problem of State

LiveView and, any good frontend framework, help us organize our front end code around encapsulating state within a component. Meaning the only place we need to think about the state of a view is in our `assigns`, there will be no other references mutating our values outside of the component.

Unfortunately in all frameworks this a lie, or a helpful fiction. All frontend frameworks actually have to deal with three(!) separate local states and keep them in sync, for example here are the 3 states for LiveView:

- Server, where we keep track of assigns, session and the WebSocket related state.
- Browser HTML DOM state.
- Browser Internal DOM state.

In react the first state would be the Component/state/props, they still need to maintain the HTML and the internal DOM State. This is why they have so many functions around `use*` now to encapsulate the many different ways you might want to manage state and the dom.

In LiveView flow of state is as follows:

1. Developer changes `assigns`.
2. LiveView calculates the minimum HTML Diff changes and sends it down the WebSocket.
3. LiveView.js sends the changes to the virtual DOM library [`morphdom`](https://github.com/patrick-steele-idem/morphdom) which alters the HTML DOM.
4. User changes inputs, which _only_ changes the Browser Internal DOM State.

Knowing this maybe we can guess what went wrong with our form, and it happens right at point 1, the `assigns` value for the `query` value never changed. No diff was calculated, the browser HTML never changed so the HTML Input `value` never changed, all the while the Browsers Internal DOM State value did change.

Here is how we can change our example to reset the value on submit:

```elixir

defmodule FormExamples.FormsLive do
  use FormExamplesWeb, :live_view

def mount(_params, _session, socket) do
    {:ok,
      socket
      |> assign(form: to_form(%{"query" => nil}))
    }
  end

def render(assigns) do
    ~H"""
    <.simple_form for={@form} phx-submit="save" phx-change="validate" id="form" class="mb-20">
      <.input field={@form[:query]} label="Search" />
    </.simple_form>
    """
  end

def handle_event("validate", %{"query" => q}, socket) do
    {:noreply, assign(socket, form: to_form(%{"query" => q}))}
  end

Here we added a `phx-change` to our form, with `handle_event` function that changes the form and query `assigns` whenever the form changes. Now when we hit `submit` our input would reset to nil because every single keypress we informed LiveView of this change! We can follow the flow of state here:

1. User changes the text box, firing a `phx-change` event.
2. Handle event `assign` changes the LiveView state.
3. LiveView calculates a diff that is essentially `set input.value to assigns.query`
4. LiveView.js and `morphdom` sets the HTML DOM value of our input.
5. Which is the same as the current HTML Internal Dom value so no change happens that the user can see.

## Enter A Server Restart

Now this is where the [Auto-Recovery](https://hexdocs.pm/phoenix_live_view/0.20.14/form-bindings.html#recovery-following-crashes-or-disconnects) Magic happens when we deploy or the [process](/content/phoenix-files/a-liveview-is-a-process/index.html) restarts:

1. On re-connect a `phx-change` event is fired with the current DOM for every Form that has the same `id` as a reconnect mounts html.
2. Handle event `assign` changes the LiveView internal state with the validate params.
3. LiveView calculates a diff that is essentially `set input.value to assigns.query`.
4. LiveView.js and `morphdom` sets the HTML DOM value of our text box.
5. Which is the same as the current HTML Internal Dom value so no change happens that the user can see.

The only difference here a little bit of Magic that LiveView does when it knows it’s reconnecting to an existing DOM State. If the current DOM has a Form with the same `id`, instead of replacing it, it fires the `change` event which should alter the LiveView’s current assigns with the previous state! And there you have it—fully recoverable forms with no server-side state at all! This works assuming the Form’s `id` remains unchanged.

And if you don’t believe me I pushed a live example [here](https://form-examples.fly.dev/) with code [here](https://github.com/jeregrine/form_examples/blob/main/lib/form_examples_web/live/forms_live.ex).

## Takeaways

1. Always give each Form a unique and consistent `id`.
2. Always have a `phx-change` event for Forms that updates the server side `assign`.

By understanding its inner workings of form handling and auto-recovery mechanisms, we can unlock the full potential of LiveView to create seamless user experiences. I encourage you to check on your own forms and inputs and make sure they have `ids` and `phx-change` events wired up to give your users a better user experience!
