mcpField
L
liveview-patterns

liveview-patterns

github.com/oliver-kriskaDevelopment57159 installsUpdated 1w ago
Repository

About liveview-patterns

A reference skill for building applications with Phoenix LiveView 1.0 and 1.1. It gives implementation patterns for asynchronous data loading, streams, forms, navigation, and component choices, with guidance on avoiding common lifecycle and memory pitfalls.

Key features

  1. 01Patterns for async assigns and data loading
  2. 02Stream-based handling of lists
  3. 03Lifecycle-aware PubSub subscriptions
  4. 04Form validation and error handling
  5. 05Navigation and component decision guides

Use cases

  1. 01Load data safely during LiveView mount
  2. 02Render large lists with streams
  3. 03Prevent duplicate PubSub subscriptions
  4. 04Choose navigation and component patterns

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix --skill liveview-patterns

After installing, type/liveview-patternsin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
async-streams.md
channels-presence.md
components.md
forms-uploads.md
js-interop.md
pubsub-navigation.md

Details

From the project README

LiveView Patterns Reference

Ash projects: Use ash-framework skill for AshPhoenix.Form. Lifecycle: AshPhoenix.Form.validate/3 on phx-change, AshPhoenix.Form.submit/2 on submit, to_form/1 for HEEx. Do not use Ecto.Changeset.cast/3.

Reference for building with Phoenix LiveView 1.0/1.1.

Iron Laws — Never Violate These

  1. NO UNCONDITIONAL DB QUERIES IN MOUNT — Mount runs TWICE. Default: assign_async. SEO routes: connected? guard + cache-backed disconnected branch (crawlers read that HTML)
  2. ALWAYS USE STREAMS FOR LISTS — Regular assigns = O(n) memory per user. Streams = O(1)
  3. CHECK connected?/1 BEFORE SUBSCRIPTIONS — Prevents double subscriptions
  4. EXTRACT VARIABLES BEFORE assign_async CLOSURE — Closures copy entire referenced variables
  5. LOAD PRIMARY DATA IN mount/3, PAGINATION IN handle_params/3 — handle_params runs on EVERY URL change
  6. NEVER PASS SOCKET TO BUSINESS LOGIC — Extract data before calling contexts
  7. CHECK CHANGESET ERRORS BEFORE UI DEBUGGING — Silent form save = check {:error, changeset} first, not viewport/JS
  8. HIDDEN INPUTS FOR ALL REQUIRED EMBEDDED FIELDS — Every required field in an embedded schema MUST have a hidden_input if not directly editable
  9. NEVER USE assign_new FOR LIFECYCLE VALUES — assign_new skips the function if key exists. Use assign/3 for locale, current user, or any value refreshed every mount
  10. MATCH {:error, %Ecto.Changeset{}} EXPLICITLY — Bare {:error, _} merges changeset and non-changeset errors; the form silently never re-renders validation errors. Handle other errors separately

Memory Impact

Pattern3K items10K users × 10K items
Regular assigns~5.1 MB~10+ GB
Streams~1.1 MBMinimal (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)
SEO Dead-Render (cache-backed disconnected branch)

For public/SEO-visible routes (marketing, articles, product listings) the disconnected render IS the HTML crawlers see. Fetch from a cache there, real data on connect:

def mount(_params, _session, socket) do
  products =
    if connected?(socket),
      do: Catalog.list_products(),
      else: Cache.get_products() || []

  {:ok, assign(socket, products: products)}
end

Empty list → <noscript>-friendly skeleton. Cache → :persistent_term, ETS, or Cachex. This satisfies Iron Law #1 AND keeps Googlebot/GPTBot happy.

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

WrongRight
DB queries without assign_asyncUse assign_async for mount queries (SEO routes: connected? + cached dead render)
assign(socket, items: list) for listsstream(socket, :items, list)
PubSub subscribe without connected?if connected?(socket), do: subscribe()
Passing socket to context functionsExtract socket.assigns first
Business logic in handle_eventDelegate to context
assign_new for locale/user in hooksassign/3 (must run every mount)

References

For detailed patterns, see:

  • ${CLAUDE_SKILL_DIR}/references/async-streams.md - assign_async, stream_async, streams
  • ${CLAUDE_SKILL_DIR}/references/forms-uploads.md - Forms, validation, file uploads
  • ${CLAUDE_SKILL_DIR}/references/components.md - Function components, LiveComponents
  • ${CLAUDE_SKILL_DIR}/references/pubsub-navigation.md - PubSub, navigation, JS commands
  • ${CLAUDE_SKILL_DIR}/references/js-interop.md - Third-party JS libraries, phx-update="ignore", hooks
  • ${CLAUDE_SKILL_DIR}/references/channels-presence.md - Phoenix Channels, Presence, token auth

Comments

Markdown supported