Lesson 19 / 25

Phoenix LiveView

Build interactive, server-rendered UIs with LiveView.

Rich interactivity without a JavaScript framework

Phoenix LiveView lets you build interactive, real-time interfaces in Elixir. A LiveView is a process on the server that holds the page's state in assigns. On first request it renders HTML (good for SEO and fast first paint), then the browser opens a WebSocket and the LiveView becomes stateful: user events (phx-click, phx-submit, phx-change) are sent to the server, handled in handle_event/3, the assigns change, and LiveView sends only the minimal diff of the HTML that changed. Key callbacks are mount/3 (initial state), handle_params/3 (URL changes), handle_event/3 (user interaction) and handle_info/2 (messages, such as PubSub broadcasts). Function components and live components structure the UI, streams efficiently render large collections without holding them in memory, and JS commands handle purely client-side effects. Forms integrate with changesets for live validation. LiveView reached version 1.0 in late 2024. Because each connected user has a process, keep assigns small, and use hooks for the occasional custom JavaScript.

How LiveView works

The server holds state, receives events and pushes minimal HTML diffs over a WebSocket.

A browser window and a server process circle connected by a two-way arrow, with small event tokens going up and small diff fragments coming down.
Figure 7.1 — LiveView events and diffs.

A live stock counter with a form

mount, handle_event and HEEx with phx bindings.

defmodule ShopWeb.StockLive do
  use ShopWeb, :live_view
  alias Shop.Inventory

  @impl true
  def mount(%{"sku" => sku}, _session, socket) do
    if connected?(socket), do: Phoenix.PubSub.subscribe(Shop.PubSub, "stock:#{sku}")
    {:ok, assign(socket, sku: sku, stock: Inventory.stock(sku), qty: 1)}
  end

  @impl true
  def handle_event("reserve", %{"qty" => qty}, socket) do
    qty = String.to_integer(qty)

    case Inventory.reserve(socket.assigns.sku, qty) do
      {:ok, remaining} -> {:noreply, socket |> assign(stock: remaining) |> put_flash(:info, "Reserved #{qty}")}
      {:error, :insufficient} -> {:noreply, put_flash(socket, :error, "Not enough stock")}
    end
  end

  @impl true
  def handle_info({:stock_changed, remaining}, socket), do: {:noreply, assign(socket, stock: remaining)}

  @impl true
  def render(assigns) do
    ~H"""
    <section>
      <h1>{@sku}</h1>
      <p>In stock: <strong>{@stock}</strong></p>
      <form phx-submit="reserve">
        <input type="number" name="qty" value={@qty} min="1" />
        <button disabled={@stock == 0}>Reserve</button>
      </form>
    </section>
    """
  end
end

Curly-brace interpolation in HEEx

Recent LiveView versions interpolate values in HEEx bodies with {@stock}, while older templates use <%= @stock %>. Attributes have always used value={@qty}. Match the style of your Phoenix version.

Quick check: After the initial render, what does LiveView send to the browser when state changes?

  • A minimal diff of the changed parts of the HTML over a WebSocket
  • A full page reload
  • A JSON API response that the client renders
  • Nothing
Answer

A minimal diff of the changed parts of the HTML over a WebSocket — LiveView tracks changes and sends compact diffs.