Phoenix Liveview
Phoenix LiveView patterns for building real-time, server-rendered interactive UIs
You are an expert in Phoenix LiveView for building real-time interactive user interfaces in Elixir. ## Key Points - **LiveView**: A process that holds state (`assigns`), renders HTML via HEEx templates, and handles events over a WebSocket. - **Lifecycle**: `mount/3` -> `handle_params/3` -> `render/1`. Events trigger `handle_event/3`. - **Assigns**: The state map available in templates via `@assign_name`. - **HEEx Templates**: HTML + Elixir expressions with compile-time validation. - **LiveComponents**: Stateful or stateless components that encapsulate markup and behaviour within a LiveView. - **Streams**: Efficient handling of large collections without holding all items in assigns. - Use `streams` for lists that can grow large — they avoid keeping the full collection in assigns. - Subscribe to PubSub topics only when `connected?(socket)` is true to avoid duplicate subscriptions during the static render. - Minimize assigns — every assign change triggers a re-render diff. Use `assign_new/3` for values that should only be set once. - Break complex UIs into LiveComponents. Use stateless function components for pure markup, stateful LiveComponents when the component needs its own event handling. - Use `push_event/3` to send data to JavaScript hooks for things LiveView cannot handle natively (e.g., clipboard, canvas, third-party JS libraries). - Debounce form validation with `phx-debounce` to reduce server round-trips.
skilldb get elixir-phoenix-skills/Phoenix LiveviewFull skill: 214 linesPhoenix LiveView — Elixir/Phoenix
You are an expert in Phoenix LiveView for building real-time interactive user interfaces in Elixir.
Overview
Phoenix LiveView enables rich, real-time UIs with server-rendered HTML. It maintains a persistent WebSocket connection between client and server, sending minimal diffs to update the DOM. This eliminates most client-side JavaScript while delivering highly interactive experiences.
Core Philosophy
LiveView inverts the traditional web architecture by keeping the UI state on the server. Each connected user has a dedicated server process that holds the current state (assigns), renders HTML, and sends minimal diffs over a WebSocket when state changes. This eliminates the need for a separate API layer, client-side state management, and the inevitable synchronization bugs that come with duplicating logic across frontend and backend.
The tradeoff is explicit and intentional: every user interaction requires a server round-trip. LiveView optimizes this by sending only the parts of the HTML that actually changed (not the full page), making these updates fast enough to feel instantaneous for most interactions. The programming model is simple — update assigns in an event handler, and the framework handles re-rendering and DOM patching automatically.
Streams represent LiveView's answer to the problem of large collections. Instead of holding thousands of items in assigns (which would be diffed on every render), streams manage client-side DOM elements efficiently with insert, update, and delete operations. This pattern is essential for feeds, tables, and any UI that displays a growing or changing list of items.
Anti-Patterns
-
Assigns as Global State: Storing every piece of data the page might need in assigns during
mount/3. This bloats the process memory, slows down diffs, and forces unnecessary re-renders. Useassign_new/3for values computed once, streams for collections, andassign_async/3for data fetched after initial render. -
Expensive Mount Queries: Running slow database queries or external API calls in
mount/3without caching or async loading. Mount runs on every page load and every WebSocket reconnect. Useassign_async/3to load slow data without blocking the initial render. -
Component Data Over-Sharing: Passing the entire socket or parent assigns into a LiveComponent instead of only the specific data it needs. This creates invisible coupling and causes unnecessary re-renders when unrelated assigns change.
-
JavaScript as Escape Hatch: Reaching for JavaScript hooks for interactions that LiveView handles natively (form validation, dynamic UI updates, conditional display). Hooks add complexity and bypass LiveView's diff engine. Reserve them for browser APIs that LiveView genuinely cannot access (clipboard, canvas, geolocation).
-
Ignoring the Static Render: Building LiveViews that only work when the WebSocket is connected. The first render is a regular HTTP response. If your
mount/3skips data loading whenconnected?/1is false, users see a blank or broken page before the WebSocket connects.
Core Concepts
- LiveView: A process that holds state (
assigns), renders HTML via HEEx templates, and handles events over a WebSocket. - Lifecycle:
mount/3->handle_params/3->render/1. Events triggerhandle_event/3. - Assigns: The state map available in templates via
@assign_name. - HEEx Templates: HTML + Elixir expressions with compile-time validation.
- LiveComponents: Stateful or stateless components that encapsulate markup and behaviour within a LiveView.
- Streams: Efficient handling of large collections without holding all items in assigns.
Implementation Patterns
Basic LiveView
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
@impl true
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
@impl true
def render(assigns) do
~H"""
<div>
<h1>Count: {@count}</h1>
<button phx-click="increment">+1</button>
</div>
"""
end
end
LiveView with Streams
defmodule MyAppWeb.TimelineLive do
use MyAppWeb, :live_view
alias MyApp.Posts
@impl true
def mount(_params, _session, socket) do
posts = Posts.list_recent(limit: 50)
{:ok, stream(socket, :posts, posts)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
post = Posts.get_post!(id)
{:ok, _} = Posts.delete_post(post)
{:noreply, stream_delete(socket, :posts, post)}
end
@impl true
def render(assigns) do
~H"""
<div id="posts" phx-update="stream">
<div :for={{dom_id, post} <- @streams.posts} id={dom_id}>
<p>{post.body}</p>
<button phx-click="delete" phx-value-id={post.id}>Delete</button>
</div>
</div>
"""
end
end
Stateful LiveComponent
defmodule MyAppWeb.FormComponent do
use MyAppWeb, :live_component
@impl true
def update(%{item: item} = assigns, socket) do
changeset = MyApp.Catalog.change_item(item)
{:ok,
socket
|> assign(assigns)
|> assign_form(changeset)}
end
@impl true
def handle_event("validate", %{"item" => params}, socket) do
changeset =
socket.assigns.item
|> MyApp.Catalog.change_item(params)
|> Map.put(:action, :validate)
{:noreply, assign_form(socket, changeset)}
end
@impl true
def handle_event("save", %{"item" => params}, socket) do
case MyApp.Catalog.create_item(params) do
{:ok, item} ->
notify_parent({:saved, item})
{:noreply, push_navigate(socket, to: socket.assigns.patch)}
{:error, changeset} ->
{:noreply, assign_form(socket, changeset)}
end
end
defp assign_form(socket, changeset) do
assign(socket, :form, to_form(changeset))
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
@impl true
def render(assigns) do
~H"""
<div>
<.form for={@form} phx-target={@myself} phx-change="validate" phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.button>Save</.button>
</.form>
</div>
"""
end
end
Real-Time Updates with PubSub
defmodule MyAppWeb.DashboardLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "metrics")
end
{:ok, assign(socket, cpu: 0, memory: 0)}
end
@impl true
def handle_info({:metrics_update, metrics}, socket) do
{:noreply, assign(socket, cpu: metrics.cpu, memory: metrics.memory)}
end
@impl true
def render(assigns) do
~H"""
<div>
<p>CPU: {@cpu}%</p>
<p>Memory: {@memory} MB</p>
</div>
"""
end
end
Best Practices
- Use
streamsfor lists that can grow large — they avoid keeping the full collection in assigns. - Subscribe to PubSub topics only when
connected?(socket)is true to avoid duplicate subscriptions during the static render. - Minimize assigns — every assign change triggers a re-render diff. Use
assign_new/3for values that should only be set once. - Break complex UIs into LiveComponents. Use stateless function components for pure markup, stateful LiveComponents when the component needs its own event handling.
- Use
push_event/3to send data to JavaScript hooks for things LiveView cannot handle natively (e.g., clipboard, canvas, third-party JS libraries). - Debounce form validation with
phx-debounceto reduce server round-trips.
Common Pitfalls
- Putting expensive queries in
mount: This runs on every page load and reconnect. Cache results or useassign_async/3for slow data fetches. - Forgetting the static render: The first render is a regular HTTP response without WebSocket. Code that relies on
connected?/1being true will not run during this phase. - Leaking assigns into components: Pass only the assigns a component needs. Avoid passing the entire socket or parent assigns.
- Not handling
phx-disconnectedstate: Users see a stale page when the WebSocket drops. Use the built-in connection status UI or add custom handling. - Large payloads in assigns: Binary data or deeply nested structures bloat the diff. Store large data in ETS or the database and pass only identifiers.
Install this skill directly: skilldb add elixir-phoenix-skills
Related Skills
Channels
Phoenix Channels and PubSub for real-time bidirectional communication
Concurrency
Elixir processes and message passing for concurrent and parallel programming
Deployment
Deploying Elixir/Phoenix applications with Mix releases, Docker, and Fly.io
Ecto
Ecto patterns for database schemas, queries, changesets, and migrations in Elixir
Genserver
GenServer patterns for stateful processes in Elixir OTP applications
Otp Supervision
OTP supervision tree design for building fault-tolerant Elixir applications