Introduction
A lot of interactive UI doesn’t really need a client-side framework. Phoenix LiveView leans on that idea: it keeps state on the server, pushes minimal HTML diffs over a WebSocket, and lets you build interactive pages without writing JavaScript yourself.
Let’s build a small task manager to see the whole loop.
How LiveView works
Instead of shipping logic to the browser, the server does the work:
- render the initial HTML on the server,
- open a WebSocket automatically,
- send user events (clicks, form submits) to the server,
- update state there,
- push back only the HTML that changed.
The browser stays thin, and there’s a single source of truth on the server.
A task manager LiveView
Here’s the core module (task_live.ex):
defmodule TaskManagerWeb.TaskLive do
use Phoenix.LiveView
alias TaskManager.Tasks
def mount(_params, _session, socket) do
tasks = Tasks.list_tasks()
{:ok, assign(socket, tasks: tasks, new_task: "")}
end
def render(assigns) do
~H"""
<div>
<h2>My Tasks</h2>
<ul>
<li :for={task <- @tasks}>
{task.title}
<button phx-click="delete" phx-value-id={task.id}>Delete</button>
</li>
</ul>
<form phx-submit="add_task">
<input type="text" name="title" value={@new_task} placeholder="New task" />
<button type="submit">Add</button>
</form>
</div>
"""
end
def handle_event("add_task", %{"title" => title}, socket) do
{:ok, task} = Tasks.create_task(%{title: title})
{:noreply, update(socket, :tasks, &[task | &1])}
end
def handle_event("delete", %{"id" => id}, socket) do
task = Tasks.get_task!(id)
{:ok, _} = Tasks.delete_task(task)
tasks = Enum.reject(socket.assigns.tasks, &(&1.id == task.id))
{:noreply, assign(socket, tasks: tasks)}
end
end
The one detail worth stressing: ~H is HEEx, not EEx. Inside HTML attributes
you interpolate with curly braces (phx-value-id={task.id},
value={@new_task}), not <%= %> — the old EEx form doesn’t compile inside
an attribute. Loops read best as a :for attribute on the element rather than
a <%= for %> block. Get those two right and the template just works.
Routing to it
In router.ex:
scope "/", TaskManagerWeb do
pipe_through :browser
live "/tasks", TaskLive
end
Visit /tasks and you have an interactive, real-time task list, with no
custom JavaScript in sight.
What you get, and what to watch
The appeal is a single source of truth: state, events and rendering all live on the server, so there’s no client/server state to keep in sync and issues are easy to trace to one place.
A few things to keep in mind:
- Connections are stateful. LiveView holds a WebSocket open per user. Elixir handles that well, but it’s a real resource to plan for.
- Latency shows. Because rendering is server-side, round-trip latency affects how snappy the UI feels — host close to your users.
- Not for offline. If the app must work offline, LiveView alone won’t cut it.
Wrapping up
LiveView is a good fit when the interactivity is real but modest: forms, lists, dashboards, live updates. You write Elixir, keep one source of truth, and skip the frontend build entirely. When you genuinely need rich offline or client-heavy behaviour, reach for a JS framework — but for a lot of pages, LiveView is less machinery for the same result.
Further reading
Have comments or want to discuss this topic?
Send an email to ~bounga/bounga.org-discuss@lists.sr.ht