LocalLiveView supports patch navigation: updating the URL and re-running
handle_params/3 without a full page reload. The state update runs in the WASM
VM, so it needs no network call; the URL change is coordinated between the Elixir
code running in WASM, the Phoenix LiveView JavaScript runtime, and the browser.
Two modes
Navigation behaves differently depending on how an LLV view is rendered.
Hosted - the LLV view is rendered as a child of a connected Phoenix LiveView. Phoenix already owns the browser history, the
popstatehandler and the click handler for patch links. LLV routes its navigation through Phoenix and listens for the resultingphx:navigateevent to re-runhandle_params/3in its own views.Standalone - the LLV view is rendered directly in a HEEx or HTML template, with no host LiveView on the page. There is nothing from Phoenix to lean on, so LLV owns navigation itself: it intercepts patch-link clicks, writes the browser history entry, and handles
popstate.
The same <.link patch={...}> markup works in both modes. <.link href={...}> and
<.link navigate={...}> are handled by Phoenix LiveView as usual and are not affected
by LLV.
handle_params/3
A view opts into navigation by exporting handle_params/3, mirroring
Phoenix.LiveView. It receives the current query params and URL.
def handle_params(params, uri, socket) do
{:noreply, assign(socket, :tab, params["tab"] || "home")}
endIt runs at mount with the initial query params, and again on every patch navigation.
push_patch/2
LocalLiveView.push_patch/2 mirrors Phoenix.LiveView.push_patch/2 and is used
to navigate from Elixir:
def handle_event("select_tab", %{"tab" => tab}, socket) do
{:noreply, push_patch(socket, to: "/dashboard?tab=#{tab}")}
endThe state update happens client-side: handle_params/3 runs in the WASM VM and
its diff is applied to the DOM with no network call. The WASM side then emits an
llv:navigate event, and how the history entry is written depends on the mode:
- Standalone - the JS layer writes the browser history entry directly. No server is involved.
- Hosted - the
llv:navigateevent hands the navigation to Phoenix, which does a server round-trip to update the history it keeps on the LiveView side. This round-trip is triggered after LLV emitsllv:navigate, not as part of thepush_patch/2state update itself.
As in a normal Phoenix LiveView, clicking a <.link patch={...}> also invokes
handle_params/3 with the new params. The behaviour is identical to LiveView, so
the same link works without any LLV-specific markup.
phx:navigate
In hosted mode, Phoenix emits a phx:navigate event whenever it performs a patch
(a patch-link click or browser back/forward). Its purpose here is to pass those
Phoenix navigation events into the LLV views, which re-run handle_params/3 in
response. Patches that LLV itself initiated via push_patch/2 are de-duplicated,
so handle_params/3 is not run twice for the same navigation.
Flow
flowchart TD
PP["push_patch/2 (LLV)"] --> WASM["handle_params/3 runs in WASM, diff to DOM"]
WASM --> EMIT["local_live_view server emits llv:navigate event"]
EMIT --> H{Hosted?}
H -- yes --> PHX["route through Phoenix, phx:navigate echo de-duplicated"]
H -- no --> HIST["write browser history directly"]
CLICK["patch link click / back-forward"] --> H2{Hosted?}
H2 -- yes --> NAV["Phoenix patches + emits phx:navigate"] --> LLV["LLV re-runs handle_params/3"]
H2 -- no --> LLVCustomizing navigation
LLVEngine.create/2 accepts an onNavigate callback in its config. It overrides
LLV's default handling of an Elixir-initiated push_patch/2, letting you take
full control of the browser history (for example, to integrate with a custom
router).
- arguments:
(href, replace)- the target URL and whether the entry should replace the current one instead of being pushed - scope: only the
push_patch/2path. Patch-link clicks and back/forward navigation are unaffected.