If you’ve ever built a tabbed interface, stepped form, or sidebar-driven navigation in Phoenix LiveView, chances are you’ve run into a subtle but frustrating issue: your component re-initializes every time you switch “pages,” even though it’s technically part of the same LiveView. This happens because changing the URL — even via push_patch — triggers a re-mount of child components unless you carefully manage patching behavior.
On the surface, push_patch and live_patch seem straightforward. They let you update the browser’s URL bar without a full page reload, triggering handle_params/3 in the LiveView so you can react accordingly. But if your LiveView isn’t architected with patch-awareness in mind, you may see unwanted resets, re-renders, or even flashes of unloaded content as assigns are re-evaluated from scratch.
Let’s take an example: a user settings page with a sidebar. Each section — Profile, Password, Notifications — is its own tab. Clicking a tab calls push_patch, updating the route to /settings/profile, /settings/password, etc. The route updates, handle_params/3 fires, and the correct section is rendered. So far so good.
But let’s say the user is midway through updating their profile. They have unsaved inputs. If they accidentally click the Notifications tab and then go back to Profile, their unsaved changes are gone. Why? Because by default, your assigns are replaced each time handle_params/3 is triggered — unless you explicitly preserve them.
Here’s where the real understanding of LiveView patching begins.
When using push_patch, you’re telling LiveView: "Change the URL, but don’t reconnect the socket." That’s important. It means you can intercept the change inside handle_params/3 and decide exactly how to respond. But LiveView has no idea which of your assigns are transient (ephemeral UI state) and which are permanent (persistent backend state). If you naively reassign all assigns every time, your form inputs and local session state will disappear.
To preserve state across patch transitions, you need to structure your LiveView to initialize its state once in mount/3 and modify it only when truly necessary inside handle_params/3. That often means adding conditional logic: if the route has changed but the underlying resource hasn’t, avoid re-fetching it or re-initializing form data.
This pattern becomes even more powerful when paired with live_component or render/2 clause isolation. You can break your interface into patch-aware components, passing only minimal params to them, and letting each component control its own reactivity. The parent LiveView handles patching and routing, while child components remain mounted and stable — keeping their state intact between transitions.
Let’s push further. Imagine a Kanban board where each column is a LiveComponent. When you click a card, the URL patches to include the card ID, and a modal opens. That modal fetches card data and shows edit controls. But when you dismiss the modal — another patch — you don’t want the entire board to re-render. You want the card data to remain where it was, any unsaved form fields intact, and only the modal interface to change. This level of UI fidelity is possible, but only if you’ve structured your assigns and patches carefully.
Another common mistake is treating handle_params/3 like mount/3. They’re not the same. mount/3 is called once per connection; handle_params/3 is called on every patch. When you mix initialization logic into both, you get unexpected resets. The best approach is to treat mount/3 as the place to set up persistent assigns, like session tokens, initial resources, or default modes. Then, inside handle_params/3, apply only the minimal changes needed to reflect the URL state — typically a param like :section, :tab, or :id.
One practical tip is to maintain a separate struct or map inside your socket assigns for transient state — form data, in-progress edits, cursor positions, etc. You then explicitly preserve or clear that struct depending on the nature of the patch. For example, switching from one profile tab to another might retain the form state if both tabs deal with the same resource, but reset it if you navigate away to a different entity. Having that separation of state responsibilities gives you much finer control over the user experience.
You’ll also find that temporary_assigns and assign_new/3 are invaluable tools in patch-aware design. The former lets you keep parts of the socket lightweight and avoid memory buildup, especially when rendering large collections. The latter lets you lazily assign values only if they haven’t already been set — exactly the kind of guard you want when dealing with reentrant patch events.
All of this boils down to one principle: treat push_patch as a signal to respond, not as a reset trigger. The most robust LiveView apps react to URL changes like a router would — incrementally, surgically, and with full awareness of existing context. If your components blow away their state every time a route changes, you’re not building a reactive interface. You’re simulating one.
Building patch-aware interfaces is more than a technical trick. It’s a UX decision. The difference between a form that resets on every tab click and one that gracefully preserves input is the difference between user frustration and trust. The more complex your app gets — multiple layers of interaction, modals, tabs, editors, previews — the more valuable patch-aware design becomes.
In production-grade LiveView apps, this is no longer optional. Your users expect to move around fluidly without penalty. That means patching responsibly, designing with component lifecycle in mind, and taking full control over how and when your assigns change. LiveView gives you the tools. The rest is design discipline.
If you’re building Phoenix LiveView apps that need to scale not just technically but in terms of team and process, I’ve written a detailed PDF guide: Phoenix LiveView: The Pro’s Guide to Scalable Interfaces and UI Patterns. It’s a 20-page manual for designing LiveView apps that are maintainable, testable, and ready for collaboration. Whether you’re solo or part of a team, this guide will help you build LiveView systems that are a joy to work on — today and in the future.