How to handle the state of an input triggering DB updates in LiveView

When a user types in some text input field, I perform a DB update (after some debouncing time).

If the DB operation fails for any reason, I should update/reset that text field with the last valid text.

In order to be able to revert the text in case of DB error, I manage the text input’s state by an assigns variable :input_foo_body:

That’s why I created a separate state for the input field, in this example input_foo_body:

<%= f = form_for @changeset, "#", [phx_change: :do_foo] %>
  <%= textarea f, :body, [value: @input_foo_body, phx_debounce: "400"] %>
</form>

If the DB update failed, I can now re-assign :input_foo_body to foo’s body value from the socket (the last value that was successfully inserted into DB):

def handle_event("do_foo", %{"foo" => foo}, socket) do
  # some code...
  case Foos.insert_or_update_foo(tenant, foo_changeset) do
    {:ok, foo} ->
      {:noreply, assign(socket, %{foo: foo})}

    _ ->
      {:noreply, assign(socket, %{input_foo_body: socket.assigns.foo.body})} # <- revert
  end
end

As said, this solution allows me to revert input_foo_body in case of DB error, however one thing is bothering me and I think is a huge smell: :input_foo_body will not correspond to what the user types => I do not update :input_foo_body in case of success, because the DB operation might take some time and I would sometimes lose my last input.

Even though the solution currently works, I’m pretty sure it is a wrong solution.

Any help is appreciated!

IMHO, if it works, it isn’t wrong … maybe not optimal, but not “wrong” :wink:

I do agree that “pretending” to track state like that is code smell, though. Something I’ve taken to doing with LiveView is using assign for stateful data, and push_event for … well … events that do not represent state but which should be reacted to in some way on the client side.

The nice thing about using events for these things is it means not having to fiddle with coercing LiveView to update your render as you are hoping, and it also allows for more sophisticated processing (e.g. asking the user if they wish to try again or start over from the currently valid text).

You could perhaps look into push_event instead?

1 Like

Thank you @aseigo for your help<3

Eventually I noticed that I probably don’t fully understand how state on text inputs work. So I want to take a small step back.

Can you have a look at this very short live module:

defmodule MyApp.PageLive do
  use MyApp, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, query: "")}
  end

  def render(assigns) do
    ~L"""
    <section class="phx-hero">
      <form phx-change="suggest">
        This is the value of @query: "<%= @query %>"<br>
        <input type="text" name="q" value="<%= @query %>"/>
      </form>
    </section>
    """
  end

  def handle_event("suggest", %{"q" => query}, socket) do
    :timer.sleep(3000)
    {:noreply, assign(socket, query: Integer.to_string(Enum.random(1..1000)))}
  end
end

:query from assigns is displayed before the input, and is also the input’s value. However, those values are different:

2020-09-07_13-14-37

I expected the input to contain “380” (as in the pic above).

This would explain why the values are different:

For any given input with focus, LiveView will never overwrite the input’s current value, even if it deviates from the server’s rendered updates.

Try adding a unique id on the form and update the assign also on success; see if this solves your issue.

Additionally, we strongly recommend including a unique HTML “id” attribute on the form. When DOM siblings change, elements without an ID will be replaced rather than moved, which can cause issues such as form fields losing focus.

From the docs.

@sfusato that’s not the case for textareas though. The behaviour is different where I can make updates in the textarea while it has focus.

Interesting. I wasn’t aware of this difference between input and textarea tags in this context. I have no idea why it acts differently or if it should.

If you want your textarea to be ignored on updates (thus not overwriting the current content), but still triggering the phx-change action, simply adding an id and phx-update="ignore" should get you there.

I just reported it and it seems to be a bug:

1 Like

I got home from the office, thought I’d plop down and take at look at this now that I have some time in the evening … and lo! Lots of great follow-up discussions, issues identified, hoorah! Love this community, and glad some of this has been sorted and we’re all learning together. Hell, I saw Jose has already commented on the bug that he can reproduce it. Amazing!

1 Like

Eventually my solution would work if I can update the textarea while typing (as said, in case of error, reset the latest valid string).

As this worked only because there was a bug (it should not be possible to update the textarea while it has focus), then I guess now I can only go for your suggestion with pushEvent, if I understood correctly.

But another solution I’d like to discuss is not handling the error at all. I mean, it’s just a string from the textarea to store into the database, how could it fail? So I might just consider it will never fail. But it means that if it would fail for any reason, what he types does not correspond to what is stored (for a few moments at least if he stopped typing). What do you think about that?

It can certainly fail, for instance if the DB is unavailable for a brief moment or there’s a network error. The question is whether the client-side string should be reset on error … it might make more sense to just show the user a non-interrupting error (e.g. in a flash associated with the text input) and then try again.

1 Like