LiveView
Phoenix LiveView — real-time server-rendered interactive views with mount, handle_event, handle_info, and HEEx templates
LiveView provides real-time server-rendered views without writing JavaScript. The server holds state and pushes diffs to the client over a persistent WebSocket connection.
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", _, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
@impl true
def handle_event("decrement", _, socket) do
{:noreply, update(socket, :count, &(&1 - 1))}
end
@impl true
def render(assigns) do
~H"""
<div>
<h1>Count: <%= @count %></h1>
<button phx-click="decrement">-</button>
<button phx-click="increment">+</button>
</div>
"""
end
endMount
mount/3 runs when the LiveView connects. It receives params, session, and socket:
@impl true
def mount(%{"id" => id}, _session, socket) do
if connected?(socket) do
# Only runs on WebSocket connect, not initial HTTP render
Phoenix.PubSub.subscribe(MyApp.PubSub, "posts:#{id}")
end
post = Blog.get_post!(id)
{:ok, assign(socket, post: post)}
end
# Redirect on mount
def mount(_params, _session, socket) do
{:ok, redirect(socket, to: ~p"/login")}
end
# Force disconnect
def mount(_params, _session, socket) do
{:error, :unauthorized}
endhandle_event
Handle client-side DOM events:
@impl true
def handle_event("save", %{"post" => post_params}, socket) do
case Blog.create_post(post_params) do
{:ok, post} ->
{:noreply,
socket
|> put_flash(:info, "Post created!")
|> push_navigate(to: ~p"/posts/#{post}")}
{:error, changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
# With JS commands
def handle_event("delete", %{"id" => id}, socket) do
{:ok, _} = Blog.delete_post(Blog.get_post!(id))
{:noreply, stream(socket, :posts, Blog.list_posts(), reset: true)}
endhandle_info
Handle Elixir messages (from PubSub, Process.send, etc.):
@impl true
def handle_info({:post_updated, post}, socket) do
{:noreply, assign(socket, post: post)}
end
# Usage: broadcast from a context
def update_post(post, attrs) do
{:ok, post} = Repo.update(Post.changeset(post, attrs))
Phoenix.PubSub.broadcast(MyApp.PubSub, "posts:#{post.id}", {:post_updated, post})
{:ok, post}
endAssigns
# Set one or more assigns
assign(socket, title: "Home", user: user)
# Update an assign with a function
update(socket, :count, &(&1 + 1))
# assign_new — set only if not present (useful in layouts)
assign_new(socket, :page_title, fn -> "Default Title" end)
# Access in templates
@title
@user.nameBinding reference
| Attribute | Event | Description |
|---|---|---|
phx-click |
"increment" |
Click event |
phx-submit |
"save" |
Form submission |
phx-change |
"validate" |
Form/input change |
phx-blur |
"lost_focus" |
Input loses focus |
phx-focus |
"gained_focus" |
Input gains focus |
phx-keydown |
"keydown" |
Key pressed |
phx-keyup |
"keyup" |
Key released |
phx-window-keydown |
"keydown" |
Global keydown listener |
Event parameters
<!-- Pass value -->
<button phx-click="delete" phx-value-id="42">Delete</button>
<!-- Pass multiple values -->
<div phx-click="select" phx-value-type="post" phx-value-id="42">
<!-- Key filters -->
<div phx-window-keydown="close" phx-key="Escape">Debounce and throttle
<!-- Debounce: wait 300ms after last event -->
<input phx-change="search" phx-debounce="300" />
<!-- Debounce until blur -->
<input phx-change="validate" phx-debounce="blur" />
<!-- Throttle: fire at most every 300ms -->
<input phx-keyup="suggest" phx-throttle="300" />Streams
Streams efficiently handle large collections by tracking only changes:
# In mount
{:ok, stream(socket, :posts, Blog.list_posts())}
# Append a new item
{:noreply, stream_insert(socket, :posts, new_post)}
# Prepend
{:noreply, stream_insert(socket, :posts, new_post, at: 0)}
# Update an item
{:noreply, stream_insert(socket, :posts, updated_post)}
# Delete an item
{:noreply, stream_delete(socket, :posts, post)}
# Reset the whole stream
{:noreply, stream(socket, :posts, Blog.list_posts(), reset: true)}Stream template
<ul id="posts" phx-update="stream">
<li :for={{id, post} <- @streams.posts} id={id}>
<%= post.title %>
</li>
</ul>Navigation
# Push a patch (same LiveView, params change)
{:noreply, push_patch(socket, to: ~p"/posts?page=#{page}")}
# Navigate to a different LiveView
{:noreply, push_navigate(socket, to: ~p"/posts/#{post.id}")}
# Redirect (non-LiveView)
{:noreply, redirect(socket, to: ~p"/login")}
# External redirect
{:noreply, redirect(socket, external: "https://example.com")}LiveView navigation vs redirect
| Function | Use when |
|---|---|
push_patch |
Change URL params within the same LiveView |
push_navigate |
Go to a different LiveView (preserves session) |
redirect |
Go to a non-LiveView page |
redirect external: |
Go to an external URL |
LiveView life cycle
HTTP request → mount/3 → render/1 → HTML response
WebSocket connect → mount/3 → render/1 → DOM diff
Event → handle_event/3 → render/1 → DOM diff
Message → handle_info/2 → render/1 → DOM diff
Params change → handle_params/3 → render/1 → DOM diff
handle_params
React to URL parameter changes (for push_patch and browser navigation):
@impl true
def handle_params(params, _url, socket) do
page = String.to_integer(params["page"] || "1")
{:noreply, assign(socket, page: page, posts: Blog.list_posts(page: page))}
endJS commands
Use phx-hook or the JS module for client-side interactions:
<!-- Toggle a class -->
<button phx-click={JS.toggle_class("hidden", to: "#details")}>Toggle</button>
<!-- Add/remove classes -->
<button phx-click={JS.add_class("active", to: "#tab")}>Activate</button>
<button phx-click={JS.remove_class("active", to: "#tab")}>Deactivate</button>
<!-- Show/hide elements -->
<button phx-click={JS.show(to: "#modal")}>Open</button>
<button phx-click={JS.hide(to: "#modal")}>Close</button>
<!-- Dispatch a custom event -->
<button phx-click={JS.dispatch("my_event", to: "#target")}>Go</button>
<!-- Focus an input -->
<button phx-click={JS.focus(to: "#search")}>Focus</button>
<!-- Chain commands -->
<button phx-click={JS.toggle_class("hidden", to: "#details") |> JS.focus(to: "#search")}>
Toggle & Focus
</button>JavaScript hooks
For complex client-side behavior, use phx-hook:
// assets/js/hooks/clock.js
export const ClockHook = {
mounted() {
this.interval = setInterval(() => {
this.pushEvent("tick", {});
}, 1000);
},
destroyed() {
clearInterval(this.interval);
}
};# In the LiveView template
<div id="clock" phx-hook="ClockHook">
<%= @time %>
</div>Temporary assigns
Mark assigns that reset to a default value after each render, reducing payload size:
def mount(_params, _session, socket) do
socket =
socket
|> assign(:page_title, "Posts")
|> assign(:posts, Blog.list_posts())
{:ok, socket, temporary_assigns: [posts: []]}
end