### Environment
* Elixir version (elixir -v): 1.16.1
* Phoenix version (mix …deps): 1.7.11
* Phoenix LiveView version (mix deps): 0.20.11
* Operating system: Fedora Silverblue 39
* Browsers you attempted to reproduce this bug on (the more the merrier): Firefox and Chrome
* Does the problem persist after removing "assets/node_modules" and trying again? Yes/no: Yes
### Actual behavior
If you have a live component that receives a `FormField` as an attribute and you restart the phoenix server, when the connection is reestablished, the live component `update/2` function will be called twice.
The first time, the form field will come with the `value` field as `nil`, the second time it will arrive with the correct value in the `value` field.
For most cases, this is not a problem, since the second `update/2` call will restore the form input value correctly.
The problem arrives when your component expects to just receive the `update/2` call once and then all the other calls will be ignored, like this:
```elixir
def update(_assigns, %{assigns: %{initialized?: true}} = socket), do: {:ok, socket}
def update(assigns, socket), do: {:ok, socket |> assign(assigns) |> assign(initialized?: true)}
```
In this case, the form will never be restored correctly because the first `update/2` call after a server restart will come with `FormField` `value` as `nil`.
In case you are wondering why I would like to do something like that, I do it for a custom select component I created to handle high latency scenarios, feel free to ask more information about it if needed and I will post the full code for it as-well.
### Expected behavior
After a server restart, I expect that LiveView will try to restore the form and call components `update/2` function that has this form/form_field as attribute only once with the correct `value` instead of calling twice with the first one being with incorrect data.
If that is not possible for some reason, can we at least pass some information or flag that indicates that this second `update/2` call is coming from a `form` restore procedure? If I had some way to differentiate a normal `update/2` call from one that is triggered during the form restoration, I would be able to pattern match it in my component and handle it correctly.
### Example
Here is a full code that will trigger the above issue:
```elixir
Application.put_env(:sample, Example.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
)
Mix.install([
{:plug_cowboy, "~> 2.5"},
{:jason, "~> 1.0"},
{:phoenix, "~> 1.7"},
{:phoenix_live_view, "~> 0.20.11"}
])
defmodule Example.ErrorView do
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
defmodule CoreComponents do
use Phoenix.Component
alias Phoenix.LiveView.JS
attr :id, :any, default: nil
attr :name, :any
attr :label, :string, default: nil
attr :value, :any
attr :type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file hidden month number password
range radio search select tel text textarea time url week)
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
attr :errors, :list, default: []
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
attr :rest, :global, include: ~w(autocomplete cols disabled form max maxlength min minlength
pattern placeholder readonly required rows size step)
slot :inner_block
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
assigns
|> assign(field: nil, id: assigns.id || field.id)
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|> assign_new(:value, fn -> field.value end)
|> input()
end
def input(assigns) do
~H"""
<div phx-feedback-for={@name}>
<input
type={@type}
name={@name}
id={@id || @name}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"mt-2 block w-full rounded-lg border-zinc-300 py-[7px] px-[11px]",
"text-zinc-900 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6",
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5",
"border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5",
@errors != [] && "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
]}
{@rest}
/>
</div>
"""
end
end
defmodule Example.Component do
use Phoenix.LiveComponent
import CoreComponents
def update(assigns, socket) do
dbg(__ENV__.function)
dbg(connected?(socket))
dbg(assigns.field.value)
{:ok, assign(socket, assigns)}
end
def render(assigns) do
~H"""
<div>
<.input type="text" field={@field} />
</div>
"""
end
end
defmodule Example.HomeLive do
use Phoenix.LiveView, layout: {__MODULE__, :live}
import CoreComponents
def mount(_params, _session, socket) do
{:ok, socket |> assign(edit: false) |> assign(form: to_form(%{"blibs" => nil}))}
end
def handle_event("validate", params, socket) do
dbg(__ENV__.function)
{:noreply, assign(socket, form: to_form(params))}
end
def render("live.html", assigns) do
~H"""
<script src={"https://cdn.jsdelivr.net/npm/phoenix@#{phx_vsn()}/priv/static/phoenix.min.js"}></script>
<script src={"https://cdn.jsdelivr.net/npm/phoenix_live_view@#{lv_vsn()}/priv/static/phoenix_live_view.min.js"}></script>
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
liveSocket.connect()
</script>
<style>
* { font-size: 1.1em; }
</style>
<%= @inner_content %>
"""
end
def render(assigns) do
alias Phoenix.LiveView.JS
~H"""
<.form id="form" for={@form} phx-change="validate" phx-submit="save">
<.live_component module={Example.Component} id="input" field={@form[:blibs]} />
</.form>
"""
end
defp phx_vsn, do: Application.spec(:phoenix, :vsn)
defp lv_vsn, do: Application.spec(:phoenix_live_view, :vsn)
end
defmodule Example.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug(:accepts, ["html"])
end
scope "/", Example do
pipe_through(:browser)
live("/", HomeLive, :index)
end
end
defmodule Example.Endpoint do
use Phoenix.Endpoint, otp_app: :sample
socket("/live", Phoenix.LiveView.Socket)
plug(Example.Router)
end
{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)
```
To reproduce it, just run this on iex, go to `localhost:5001`, type something in the text input element, kill iex, run iex again with the above code and wait for the browser to reconnect the socket, you will see something like this in the terminal:
```elixir
13:12:46.142 [debug] Replied in 2ms
[iex:15: Example.Component.update/2]
__ENV__.function #=> {:update, 2}
[iex:16: Example.Component.update/2]
connected?(socket) #=> true
[iex:17: Example.Component.update/2]
assigns.field.value #=> nil
13:12:46.156 [debug] HANDLE EVENT "validate" in Example.HomeLive
Parameters: %{"_target" => ["blibs"], "blibs" => "fewfwefe"}
[iex:21: Example.HomeLive.handle_event/3]
__ENV__.function #=> {:handle_event, 3}
13:12:46.156 [debug] Replied in 279µs
[iex:15: Example.Component.update/2]
__ENV__.function #=> {:update, 2}
[iex:16: Example.Component.update/2]
connected?(socket) #=> true
[iex:17: Example.Component.update/2]
assigns.field.value #=> "fewfwefe"
```
Which shows the 2 `update/2` calls after server is restarted.