JohnnyCurran

JohnnyCurran

The Top 3 LiveView Form Mistakes (And How to Fix Them)

I’ve been writing LiveView since 2020. In that time, I’ve seen the same three form mistakes at multiple companies. Here’s what they are and how to fix them.

1. Slow, laggy forms with scattered logic because form state gets stored in socket assigns and server round-trips get used for dynamic UI (conditional inputs, toggles), instead of keeping that state in hidden form inputs where it belongs.

2. Brittle system where UI and database can’t evolve independently because database schemas get used directly for forms, coupling persistence logic to presentation.

3. Users stuck with valid data but can’t submit because changesets get manually manipulated with Map.put or Map.merge instead of Ecto.Changeset functions, leaving stale errors behind.

The common thread: don’t fight the framework. Keep form state on the client, create embedded schemas for your forms, and use Ecto.Changeset functions to modify changesets.

Most Liked

kevinschweikert

kevinschweikert

In Mistake #2, where you do:

 {:error, changeset} ->
      {:noreply, assign(socket, :form, to_form(changeset))}

How would you map the errors back to the form schema, when the fields from the DB are different?

tfwright

tfwright

I don’t have much professional experience with LiveView, just hobby projects. But RE 1, aren’t things a bit more complex than you suggest? You say “don’t fight the framework,” but naively it would seem using JS at all is fighting a framework the express intention of which is presumably to manage state on the server. Your example is a good one for your argument, seems like pure FE state that is relatively easy to handle (with latest JS integration). But in reality isn’t most state more ambiguous?

Take a table with rows that are selectable. On first glance this seems like something that should be handled on the FE. Certainly requiring a server trip to toggle selection creates lag, and arguably breaks a strong user expectation that checkboxes are highly responsive. But what happens when some BE action needs to know which rows are selected? Or even needs to rerender part of the view (e.g. to display some metadata about selected rows)? Obviously, you can solve this with more JS, but now all the defects you mentioned with a BE implementation apply: the logic is more spread out, more place for bugs, and arguably JS bugs are a lot more difficult to test and QA against (at least, that’s why we’re using LV in the first place, no?). Alternatively, one could try to use different tooling to alleviate issues with lag, like debouncing, optimistic UI updates, etc.

To be clear, I don’t think your advice is necessarily bad, in the end I think it is simply one of the challenges of development with LV to find the right balance here. But it is a balance, and a delicate one. The more JS features get added to a LV project the lower the ROI it seems. At a certain point, if you want to add a lot of these features, DX is going to go downhill in comparison with React.

JohnnyCurran

JohnnyCurran

Good question.

In that specific case branch, nothing needs to be done, because that changeset is the form changeset.

If there were an error in Accounts.register_user and you got a changeset back, you’d do something like (psuedo-ish code):

changeset = FormModule.changeset(%FormModule{}, form_params)
changeset
|> Ecto.Changeset.apply_action(:insert)
|> case do
  {:ok, form_params} ->
    form_params
    |> Map.from_struct()
    |> Accounts.register_user()
    |> case do
      {:error, user_changeset} ->
         # Figure out which user changeset field had an error
         # Place error in changeset, validate, re-assign
         socket =
           changeset
           |> Ecto.Changeset.put_error(:field, "There was an error saving to the database!")
           |> Map.put(:action, :validate)
           |> to_form()
           |> then(&assign(socket, :form, &1))
         
        # ... rest of handler

Is how I’ve done it before

JohnnyCurran

JohnnyCurran

To be clear, I wasn’t and don’t advocate to manage frontend state on the frontend :slight_smile:

Rather, don’t separate related (form, in this case) state in multiple places (regular assigns and the @form assign), and you can use Phoenix.LiveView.JS to provide instant visual feedback to the user to provide for good UI/UX while the server W.S. Round Trip happens :slight_smile:

Thank you for raising the points you did, I think it’s turned into a good discussion! I’ll look it over and see if I can’t make that more clear for future readers

garrison

garrison

You are spot on, and in fact this problem has come up on here several times in the past. Particularly when it comes to LiveView’s imperative APIs (stream() and JS). The simpler your app is the easier it is to get away with this. This is why programming in the imperative style is so insidious: when you write code with O(N^2) paths things start off easy (when the app is simple and new) and then you get absolutely destroyed when the curve goes vertical.

The reality is that if you want to do server rendering you need to commit to it and accept the latency. Most of the time this is fine. If in your case the latency is not fine, that is a very good hint that you should not be doing server rendering.

Of course there are always exceptions, edge cases, and compromises that have to made. This is merely a guiding principle.

The main issue is that in practice the JS is usually written in an imperative style. If you were to write your JS declaratively using a proper frontend framework (e.g. React and others) and glue LiveView to that framework properly (passing LV state into props and so on) you could get away with it.

There have been several attempts to make this integration more natural (see live_vue, live_svelte, etc). You will still have consistency issues, but those can be dealt with.

Where Next?

Popular in Blog Posts Top

paulanthonywilson
https://furlough.merecomplexities.com/elixir/otp/tdd/2021/03/18/test-driving-otp-creating-a-registry-with-expiring-entries.html Followin...
New
brainlid
You are storing some Phoenix LiveView state in the browser. You want to retrieve that saved state as early as possible to improve the use...
New
brainlid
There is a new community resource available on writing “Safe Ecto Migrations”. When we get a migration wrong, it can lock up your product...
New
ErlangSolutions
An infographic that compares Erlang, Elixir, and Go's strengths in the respect to the programming languages' concurrency, reliability, sc...
New
wmnnd
Here’s the story how one of the world’s first production deployments of LiveView came to be - and how trying to improve it almost caused ...
New
stryrckt
Support for AlpineJS in LiveView was added in 0.13.3 and it works fabulously. I just wrote a blog article about it and plan another one s...
New
AstonJ
Just finished doing a clean install of macOS (which I highly recommend btw!) and have updated my macOS Ruby & Elixir/Erlang dev env s...
New
sheharyarn
In this article, I talk about the Elixir library Delta we at Slab just open-sourced, its various features including support for Operation...
New
New
New

Other popular topics Top

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
273 38985 115
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement