Claude-elixir-phoenix liveview-patterns
Build LiveView: async data (assign_async), PubSub (check connected?), phx-change events, form components/modals/uploads, streams for lists, live_patch. Use when handling interactions, debugging events, or tracking Presence.
install
source · Clone the upstream repo
git clone https://github.com/oliver-kriska/claude-elixir-phoenix
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/oliver-kriska/claude-elixir-phoenix "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/elixir-phoenix/skills/liveview-patterns" ~/.claude/skills/oliver-kriska-claude-elixir-phoenix-liveview-patterns && rm -rf "$T"
manifest:
plugins/elixir-phoenix/skills/liveview-patterns/SKILL.mdsource content
LiveView Patterns Reference
Reference for building with Phoenix LiveView 1.0/1.1.
Iron Laws — Never Violate These
- NO DATABASE QUERIES IN DISCONNECTED MOUNT — Queries run TWICE (HTTP + WebSocket). Use
assign_async - ALWAYS USE STREAMS FOR LISTS — Regular assigns = O(n) memory per user. Streams = O(1)
- CHECK connected?/1 BEFORE SUBSCRIPTIONS — Prevents double subscriptions
- EXTRACT VARIABLES BEFORE assign_async CLOSURE — Closures copy entire referenced variables
- LOAD PRIMARY DATA IN mount/3, PAGINATION IN handle_params/3 — handle_params runs on EVERY URL change
- NEVER PASS SOCKET TO BUSINESS LOGIC — Extract data before calling contexts
- CHECK CHANGESET ERRORS BEFORE UI DEBUGGING — Silent form save = check
first, not viewport/JS{:error, changeset} - HIDDEN INPUTS FOR ALL REQUIRED EMBEDDED FIELDS — Every required field in an embedded schema MUST have a
if not directly editablehidden_input - NEVER USE
FOR LIFECYCLE VALUES —assign_new
skips the function if key exists. Useassign_new
for locale, current user, or any value refreshed every mountassign/3
Memory Impact
| Pattern | 3K items | 10K users × 10K items |
|---|---|---|
| Regular assigns | ~5.1 MB | ~10+ GB |
| Streams | ~1.1 MB | Minimal (O(1)) |
Decision: Lists with >100 items → Use streams, not assigns
Quick Patterns
Async Assigns (CRITICAL)
def mount(%{"slug" => slug}, _session, socket) do # Extract needed values BEFORE the closure scope = socket.assigns.current_scope {:ok, socket |> assign_async(:org, fn -> {:ok, %{org: fetch_org(scope, slug)}} end)} end
Streams for Lists
def mount(_params, _session, socket) do {:ok, stream(socket, :items, Items.list_items())} end # Insert/update/delete stream_insert(socket, :items, item, at: 0) stream_delete(socket, :items, item)
PubSub with connected? check
def mount(_params, _session, socket) do if connected?(socket), do: Chat.subscribe(room_id) {:ok, socket} end
Navigation Decision Tree
Same LiveView, different params? → patch / push_patch Different LiveView, same live_session? → navigate / push_navigate Different live_session or non-LiveView? → href / redirect
Component Decision Tree
Does component need BOTH internal state AND event handling? │ ├── YES → Does it encapsulate APPLICATION logic (not just DOM)? │ ├── YES → Use LiveComponent ✅ │ └── NO → Refactor to function component with parent handling │ └── NO → Use Function Component ✅
Official guidance: "Prefer function components over live components"
Common Anti-patterns
| Wrong | Right |
|---|---|
DB queries without | Use for all queries |
for lists | |
PubSub subscribe without | |
| Passing socket to context functions | Extract first |
Business logic in | Delegate to context |
for locale/user in hooks | (must run every mount) |
References
For detailed patterns, see:
- assign_async, stream_async, streams${CLAUDE_SKILL_DIR}/references/async-streams.md
- Forms, validation, file uploads${CLAUDE_SKILL_DIR}/references/forms-uploads.md
- Function components, LiveComponents${CLAUDE_SKILL_DIR}/references/components.md
- PubSub, navigation, JS commands${CLAUDE_SKILL_DIR}/references/pubsub-navigation.md
- Third-party JS libraries, phx-update="ignore", hooks${CLAUDE_SKILL_DIR}/references/js-interop.md
- Phoenix Channels, Presence, token auth${CLAUDE_SKILL_DIR}/references/channels-presence.md