Phoenix LiveView Patterns
LiveView Lifecycle
A LiveView goes through two phases:
-
Static Mount: Initial HTTP request (connected?: false)
-
Connected Mount: WebSocket upgrade (connected?: true)
def mount(_params, _session, socket) do if connected?(socket) do # Subscribe to topics, start timers, etc. Phoenix.PubSub.subscribe(MyApp.PubSub, "topic") end
{:ok, assign(socket, :data, [])} end
Handle Event
Use pattern matching in handle_event/3 for different actions.
def handle_event("save", %{"post" => post_params}, socket) do case Posts.create_post(post_params) do {:ok, post} -> {:noreply, socket |> put_flash(:info, "Created!") |> assign(:post, post)} {:error, changeset} -> {:noreply, assign(socket, :changeset, changeset)} end end
def handle_event("delete", %{"id" => id}, socket) do Posts.delete_post(id) {:noreply, assign(socket, :posts, Posts.list_posts())} end
Handle Info
Handle async messages and PubSub broadcasts with handle_info/2 .
def handle_info({:post_created, post}, socket) do {:noreply, update(socket, :posts, fn posts -> [post | posts] end)} end
def handle_info(%{event: "presence_diff"}, socket) do {:noreply, assign(socket, :online_users, get_presence_count())} end
Socket Assigns
Use assign/2 or assign/3 to update socket state.
Single assign
socket = assign(socket, :count, 0)
Multiple assigns
socket = assign(socket, count: 0, name: "User", active: true)
Update existing assign
socket = update(socket, :count, &(&1 + 1))
Temporary Assigns
Use temporary assigns for large collections that don't need to persist.
def mount(_params, _session, socket) do socket = assign(socket, :posts, []) {:ok, socket, temporary_assigns: [posts: []]} end
LiveView Uploads
Use built-in upload functionality for file uploads.
def mount(_params, _session, socket) do socket = socket |> assign(:uploaded_files, []) |> allow_upload(:image, accept: ~w(.jpg .jpeg .png), max_entries: 5, max_file_size: 10_000_000 )
{:ok, socket} end
def handle_event("validate", _params, socket) do {:noreply, socket} end
def handle_event("save", _params, socket) do uploaded_files = consume_uploaded_entries(socket, :image, fn %{path: path}, entry -> dest = Path.join("priv/static/uploads", entry.client_name) File.cp!(path, dest) {:ok, "/uploads/#{entry.client_name}"} end)
{:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))} end
Flash Messages
Use put_flash/3 and clear_flash/2 for user feedback.
def handle_event("save", params, socket) do case save_data(params) do {:ok, _} -> socket = put_flash(socket, :info, "Saved successfully!") {:noreply, socket} {:error, _} -> socket = put_flash(socket, :error, "Failed to save") {:noreply, socket} end end
Live Navigation
Use push_navigate/2 or push_patch/2 for navigation.
Full page reload (new LiveView)
{:noreply, push_navigate(socket, to: ~p"/users")}
Patch (same LiveView, different params)
{:noreply, push_patch(socket, to: ~p"/posts/#{post}")}
Handle Params
Use handle_params/3 to respond to URL changes.
def handle_params(%{"id" => id}, _uri, socket) do post = Posts.get_post!(id) {:noreply, assign(socket, :post, post)} end
def handle_params(_params, _uri, socket) do {:noreply, socket} end
Streams
Use streams for efficient rendering of large lists.
def mount(_params, _session, socket) do {:ok, stream(socket, :posts, Posts.list_posts())} end
def handle_event("add", %{"post" => attrs}, socket) do {:ok, post} = Posts.create_post(attrs) {:noreply, stream_insert(socket, :posts, post, at: 0)} end
def handle_event("delete", %{"id" => id}, socket) do Posts.delete_post(id) {:noreply, stream_delete_by_dom_id(socket, :posts, "posts-#{id}")} end
Components
Extract reusable UI into function components.
def card(assigns) do ~H""" <div class="card"> <h3><%= @title %></h3> <p><%= @content %></p> </div> """ end
Usage in template
<.card title="Hello" content="World" />
Form Binding
Bind forms to changesets for validation.
<.simple_form for={@form} phx-change="validate" phx-submit="save"> <.input field={@form[:title]} label="Title" /> <.input field={@form[:body]} type="textarea" label="Body" /> <:actions> <.button>Save</.button> </:actions> </.simple_form>
def mount(_params, _session, socket) do changeset = Post.changeset(%Post{}, %{}) {:ok, assign(socket, form: to_form(changeset))} end
def handle_event("validate", %{"post" => params}, socket) do changeset = %Post{} |> Post.changeset(params) |> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))} end
Error Handling
Always handle errors gracefully in LiveViews.
def handle_event("risky_operation", _params, socket) do case perform_operation() do {:ok, result} -> {:noreply, assign(socket, :result, result)} {:error, reason} -> {:noreply, put_flash(socket, :error, "Operation failed: #{reason}")} end end
PubSub Broadcasting
Use PubSub for real-time updates across LiveViews.
Subscribe in mount
def mount(_params, _session, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(MyApp.PubSub, "posts") end {:ok, assign(socket, :posts, list_posts())} end
Broadcast when data changes
def create_post(attrs) do with {:ok, post} <- Repo.insert(changeset) do Phoenix.PubSub.broadcast(MyApp.PubSub, "posts", {:post_created, post}) {:ok, post} end end
Handle broadcast
def handle_info({:post_created, post}, socket) do {:noreply, update(socket, :posts, fn posts -> [post | posts] end)} end
Testing LiveViews
Use Phoenix.LiveViewTest for testing.
test "uploads and displays image", %{conn: conn} do {:ok, lv, _html} = live(conn, "/gallery")
image = file_input(lv, "#upload-form", :image, [ %{name: "test.png", content: File.read!("test/support/test.png")} ])
assert render_upload(image, "test.png") =~ "100%"
lv |> form("#upload-form") |> render_submit()
assert has_element?(lv, "img[alt='test.png']") end
Callbacks Implementation
Always add @impl true before callback implementations.
defmodule MyAppWeb.PostLive do use MyAppWeb, :live_view
@impl true def mount(_params, _session, socket) do {:ok, assign(socket, :posts, [])} end
@impl true def handle_event("delete", %{"id" => id}, socket) do # ... end
@impl true def render(assigns) do ~H""" <div>Content</div> """ end end