Elixir: Phoenix LiveView
LiveView renders HTML on the server and keeps a persistent WebSocket per session. When state
changes, the server sends a minimal diff — no client framework, no API layer, and the UI stays in
sync across every connected browser. One LiveView process per session is just a
GenServer with rendering attached.
The LiveView Lifecycle
defmodule MyAppWeb.CounterLive do
# The macro wires routing, rendering, and the process behaviour.
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
# Subscribe this LiveView to cluster-wide updates.
if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, "counter")
{:ok, assign(socket, count: 0)}
end
@impl true
# Events from phx- bindings in the template land here.
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
@impl true
# Broadcasts from OTHER sessions arrive as info messages.
def handle_info({:tick, count}, socket) do
{:noreply, assign(socket, count: count)}
end
end
Templates and Components
<!-- counter_live.heex — HEEx templates are validated at compile time:
typo'd attribute names and wrong HTML nesting fail the build. -->
<div id="counter">
<p>Count: <%= @count %></p>
<!-- phx- bindings turn DOM events into LiveView messages -->
<button phx-click="increment">+1</button>
<!-- function components: any assign-attributed function module -->
<MyAppWeb.Components.badge value={@count} />
</div>
# A function component — a function that takes assigns and returns a template.
defmodule MyAppWeb.Components do
use Phoenix.Component
def badge(assigns) do
~H"""
<span class={if @value > 9, do: "badge hot", else: "badge"}><%= @value %></span>
"""
end
end
Streams: Large Lists
Assigning a 5,000-row list means LiveView re-diffs the whole collection. stream sends
only the inserted/deleted rows — constant memory per collection, indexed DOM patches.
@impl true
def handle_info({:new_bid, bid}, socket) do
# stream_insert patches ONE row in the DOM; no full-list diff.
{:noreply, stream_insert(socket, :bids, bid)}
end
# In the template:
# <div id="bids" phx-update="stream">
# <div :for={{id, bid} <- @streams.bids} id={id}>{bid.price}</div>
# </div>
State-Ownership Patterns
- LiveView process per session — the default; ephemeral UI state lives in assigns.
- Shared state in a named GenServer — the LiveView subscribes to it (PubSub) instead of owning it, so 100 sessions share one price feed.
- Presence — tracks who is connected per topic, cluster-wide, using the same PubSub machinery.
The LiveView module is the client API of the pattern you already know: the process holds state, PubSub spreads events, and the browser gets diffs. Everything else on the page is ordinary Elixir.
Practice
- Build the counter above, open two browser tabs, and broadcast increments across them.
- Convert a large list render to
stream/3and watch the message size shrink in the browser inspector. - Add a form with
phx-submitand validate through an embedded-schema changeset.
Next: Designing Elixir Systems