I’ve been digging into why Phoenix LiveView’s (forms, especially) sometimes “break” and become non-responsive.
It was a bit of a deep dive and I couldn’t find a really good synopsis anywhere, so… documenting my findings here and looking for any added advice, suggestions, and commentary. Mostly, putting this out there for discussion & to help anyone else looking into similar problems. (If there are good resources on this problem, I’d love a pointer to them).
Scenario
- I have a relative simply LiveView form, and it does a little bit of dynamic rendering (offering hints and whatnot while filling out the form).
- Sometimes, the page becomes totally non-responsive.
- There’s nothing in the logs or server output to indicate failure. In fact, the logs all say “everything’s great!” right up to the point of failure… then, silence.
Cause
It probably comes down to either:
- The DOM getting confused due to dynamic updates (LiveView patches).
- Something else.
(I’m putting something else because I’m sure there are other situations that will break your LiveView connection, but this is a dive into dynamic DOM updates causing a specific problem).
Diagnosis & solution
This is a classic LiveView issue related to DOM patching conflicts when elements are conditionally rendered. Here’s what’s likely happening:
The Problem:
Given a line similar to this:
<p :if={@message} class={@class}>{raw(@message)}</p>
When the <p>
tag transitions from “not rendered” (e.g. :if={nil}
is falsy) to “rendered” (becomes truthy), LiveView’s DOM diffing algorithm can get confused, especially if:
- There are form inputs nearby that have focus or pending changes
- The newly rendered element affects the DOM structure in a way that conflicts with pending updates
- There’s a race condition between the form validation and the DOM update
Common Causes:
- Focus conflicts: If a user is typing in a form field when the DOM structure changes
- Event timing: The
phx-change
event fires, updates the socket, but the DOM patch conflicts with the browser’s form state - Input validation loops: Rapid state changes causing conflicting patches
Debugging Steps:
Online resources suggested all of the following, but none were useful. I never caught an error in the browser console, and the server-side logs/output looked just fine. But, clearly, the socket broke because all events stopped flowing.
- Check browser console for specific LiveView errors during the disconnect
- Add temporary logging around the state transition (this was informative, but ultimately did not point out the root cause).
- Check for rapid fire events: Look for multiple
phx-change
events firing in quick succession
Common Solutions:
- Use stable DOM structure … this is the simple win that I ended up going with. It makes for a more stable DOM structure, hence less confusion in DOM rendering, avoiding the problem:
<!-- Instead of conditional rendering -->
<p class={if @message, do: "@class", else: "hidden"}>
<!-- Always render, just hide/show -->
{raw(@message)}
</p>
- Add a small delay to debounce rapid changes (I did not try this):
<.input phx-change="validate" phx-debounce="300" />
- Use
phx-update="ignore"
on problematic elements (this prevents the socket from breaking, but it also prevents the inner<p>
tag from updating… so, better insofar as avoiding a break, but not useful for dynamically rendered elements):
<div phx-update="ignore" id="stable-hint">
<!-- Oops... no more dynamic changes -->
<p :if={@message} class={@class}>{raw(@message)}</p>
</div>
- Separate the state update from the rendering (another one that I didn’t try, but I’d love to hear from anyone on whether this strategy works or not… and, compared to #1, what’s regarded as “best practice”):
# In handle_event
socket =
socket
|> assign(show_hint: stereotype_id != nil)
|> assign(form: updated_form)
So, in my case, the root cause was that making DOM elements appear/disappear interfered with LiveView’s ability to cleanly patch the DOM, especially if there are active form interactions happening simultaneously.
Lesson learned: If I’m patching the DOM, try to keep it stable (don’t wholesale remove elements, instead, hide them). If that’s not possible, then… look into some variation on strategies 3 and 4.