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

Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
rocket4ce
A comprehensive guide for deploying Phoenix 1.8 applications using Coolify on Hetzner servers. Covers server setup, Coolify configuration...
New
marcelo
Long story short, over the past years we’ve been scaling our operation quite a lot at Unbabel. Most of our codebase is Python and some of...
New
New
brainlid
On your LiveView page, you are using a custom component. You want to be able to pass HTML attributes into the component, but the componen...
New
New
aymanosman
The desire to produce structured logs is common. In this article, I will survey the major approaches one could take to achieve this goal ...
New
victorbjorklund
I’m showing you how you can customise the phx.new generator to give you a new Phoenix project the way YOU want it. In this post I show yo...
New
gaggle
This post explores different ways to do test automation in Elixir, focusing on how to handle dependency injection — covering patterns, li...
New
mssantosdev
Our take on how to build a frontend style guide with Phoenix Components, Atomic Design and plain CSS, with focus on reusability and code ...
New

Other popular topics Top

lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
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