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
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
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
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
Popular in Blog Posts
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









