| name | liveview-patterns |
| description | Phoenix LiveView UI and real-time feature patterns |
LiveView Patterns Skill
Use this skill when:
- Building LiveView interfaces
- Implementing real-time features
- Designing component-based UI
- Optimizing LiveView performance
- Handling state management
- Working with Phoenix PubSub
Core Patterns
1. Optimistic UI Updates
# ✅ Good: Optimistic updates with rollback
defmodule MyAppWeb.Live.UserForm do
use MyAppWeb, :live_view
@impl true
def handle_event("save", params, socket, assign) do
# Optimistically update UI
{:noreply, assign(socket, :saving, true)}
end
@impl true
def handle_info({:update_result, :success}, socket, assign) do
{:noreply, assign(socket, :saving, false)}
end
@impl true
def handle_info({:update_result, :error}, socket, assign) do
{:noreply, put_flash(socket, :error, "Failed to save")}
end
end
# ❌ Bad: Block until save completes
defmodule MyAppWeb.Live.BadForm do
use MyAppWeb, :live_view
def handle_event("save", params, socket, assign) do
# UI blocks during save
{:noreply, assign(socket, :disabled, true)}
end
end
2. LiveView Streams
# ✅ Good: Use streams for large lists
defmodule MyAppWeb.Live.UserList do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :users, stream(socket, MyApp.Users.list_users())}
end
end
# ❌ Bad: Render entire list in memory
defmodule MyAppWeb.Live.BadUserList do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :users, MyApp.Users.list_users())}
end
end
3. Component Pattern
# ✅ Good: Reusable function components
defmodule MyAppWeb.CoreComponents do
use Phoenix.Component
attr :id, :string, required: true
attr :title, :string
attr :class, :string, default: ""
def button(assigns) do
~H"""
<button
type="button"
id={@id}
class={@class}
>
{@title}
</button>
"""
end
def user_card(assigns) do
~H"""
<div class="user-card #{@class}">
<h2><%= @title %></h2>
<p><%= @description %></p>
<.user_email><%= @email %></.user_email>
</div>
"""
end
end
# ❌ Bad: Monolithic LiveView
defmodule MyAppWeb.Live.UserCard do
use MyAppWeb, :live_view
@impl true
def render(assigns) do
~H"""
<div class="user-card">
<!-- Large component rendering inline -->
<h2><%= @user.name %></h2>
<p><%= @user.email %></p>
<%= if @user.admin? do %>
<span class="badge">Admin</span>
<% end %>
</div>
"""
end
end
4. PubSub Integration
# ✅ Good: Subscribe to PubSub topics
defmodule MyAppWeb.Live.Dashboard do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
MyApp.PubSub.subscribe(socket, "users_updates")
end
{:ok, assign(socket, :users, [])}
end
@impl true
def handle_info({:user_updated, user}, socket, assign) do
{:noreply, update(socket, :users, fn users -> [user | users])}
end
@impl true
def handle_event("delete", %{"id" => id}, socket, assign) do
MyApp.Users.delete_user(id)
MyApp.PubSub.broadcast("users_updates", {:user_deleted, id})
end
@impl true
def terminate(_reason, socket) do
if connected?(socket) do
MyApp.PubSub.unsubscribe(socket, "users_updates")
end
end
end
# ❌ Bad: Polling for updates
defmodule MyAppWeb.Live.BadDashboard do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
# Polling is inefficient
send(self(), :update_users)
{:ok, assign(socket, :users, [])}
end
@impl true
def handle_info(:update_users, socket, assign) do
users = MyApp.Users.list_users()
{:noreply, assign(socket, :users, users)}
end
end
Performance Optimization
1. Upload Handling
# ✅ Good: Chunked uploads with progress
defmodule MyAppWeb.Live.FileUpload do
use MyAppWeb, :live_view
@impl true
def handle_event("select_files", params, socket, assign) do
upload_files(params.files)
{:noreply, socket}
end
@impl true
def handle_info({:upload_progress, :file_id, :progress}, socket, assign) do
{:noreply, assign(socket, uploads: update_upload(socket.assigns.uploads, file_id, progress))}
end
defp upload_files(files) do
files
|> Enum.chunk_every(5) # Process in chunks of 5
|> Enum.each(fn chunk ->
send(self(), {:upload_chunk, chunk})
end)
end
end
# ❌ Bad: Upload all files at once
defmodule MyAppWeb.Live.BadFileUpload do
use MyAppWeb, :live_view
@impl true
def handle_event("select_files", params, socket, assign) do
# Blocks while uploading
Enum.each(params.files, fn file ->
MyApp.Storage.upload(file)
end)
{:noreply, socket}
end
end
2. Debouncing Events
# ✅ Good: Debounce rapid events
defmodule MyAppWeb.Live.Search do
use MyAppWeb, :live_view
@impl true
def handle_event("search", %{"query" => query}, socket, assign) do
debounce_search(query, 300)
end
defp debounce_search(query, delay_ms) do
# Only trigger search after delay
send_after(self(), {:search, query}, delay_ms)
end
@impl true
def handle_info({:search, query}, socket, assign) do
results = MyApp.Search.search(query)
{:noreply, assign(socket, :results, results)}
end
end
# ❌ Bad: No debouncing
defmodule MyAppWeb.Live.BadSearch do
use MyAppWeb, :live_view
@impl true
def handle_event("search", %{"query" => query}, socket, assign) do
# Every keystroke triggers search
results = MyApp.Search.search(query)
{:noreply, assign(socket, :results, results)}
end
end
State Management
1. GenServer for Global State
# ✅ Good: Use GenServer for complex state
defmodule MyApp.GlobalState do
use GenServer
def start_link(_opts) do
GenServer.start_link(__MODULE__, %{})
end
# Client API
def get_user_count do
GenServer.call(__MODULE__, :get_count)
end
def update_user_count(delta) do
GenServer.cast(__MODULE__, {:update, delta})
end
# Server callbacks
@impl true
def init(state) do
{:ok, %{state | user_count: 0}}
end
@impl true
def handle_cast({:update, delta}, state) do
new_count = state.user_count + delta
{:noreply, %{state | user_count: new_count}}
end
@impl true
def handle_call(:get_count, _from, state) do
{:reply, state.user_count, state}
end
end
2. Assigns for Local State
# ✅ Good: Use assigns for simple state
defmodule MyAppWeb.Live.Counter 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", _params, socket, assign) do
{:noreply, update(socket, :count, socket.assigns.count + 1)}
end
@impl true
def handle_event("decrement", _params, socket, assign) do
if socket.assigns.count > 0 do
{:noreply, update(socket, :count, socket.assigns.count - 1)}
else
{:noreply, socket}
end
end
end
# ❌ Bad: Using external state management
defmodule MyAppWeb.Live.BadCounter do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, get_count_from_external_service())}
end
@impl true
def handle_event("increment", _params, socket, assign) do
# External service call
new_count = get_count_from_external_service() + 1
{:noreply, assign(socket, :count, new_count)}
end
end
Real-Time Features
1. Live Navigation
# ✅ Good: Use Live navigation with handle_params
defmodule MyAppWeb.Live.Users do
use MyAppWeb, :live_view
@impl true
def handle_params(%{"id" => id}, _uri, socket, assign) do
{:noreply, assign(socket, :user, MyApp.Users.get(id))}
end
end
2. Live Notifications
# ✅ Good: PubSub-based live notifications
defmodule MyAppWeb.Live.Notifications do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
MyApp.PubSub.subscribe(socket, "notifications")
{:ok, assign(socket, :notifications, [])}
end
@impl true
def handle_info({:new_notification, notification}, socket, assign) do
{:noreply, update(socket, :notifications, [notification | socket.assigns.notifications])}
end
end
Best Practices
1. Component Design
- Keep components small and focused
- Use slots for flexibility
- Leverage Phoenix.Component for reusability
- Document public components
2. Performance
- Use streams for large data sets
- Implement debouncing for rapid events
- Use optimistic updates with rollback
- Chunk file uploads
- Avoid unnecessary re-renders
3. PubSub
- Subscribe on mount, unsubscribe on terminate
- Use topic-based subscriptions
- **Broadcast for state changes
- Handle disconnects gracefully
4. State Management
- Use assigns for simple state
- Use GenServer for complex global state
- Avoid external state for LiveView-local state
- Implement proper cleanup on terminate
5. Accessibility
- Use ARIA attributes
- Provide keyboard navigation
- Include proper labels for forms
- Test with screen readers
Token Efficiency
Use LiveView patterns for:
- Real-time UI updates (~50% token savings vs page refreshes)
- Optimistic updates (~30% token savings vs blocking operations)
- Component reusability (~40% token savings vs code duplication)
- Stream handling (~60% token savings vs rendering large lists)