code-quality
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when writing LiveView modules or HEEx templates — mount/handle_params lifecycle, forms with to_form and phx-change, streams, assign_async/start_async, JS commands, hooks, function components.
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill phoenix-liveview-essentials --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/phoenix-liveview-essentialsContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when writing LiveView modules or HEEx templates — mount/handle_params lifecycle, forms with to_form and phx-change, streams, assign_async/start_async, JS commands, hooks, function components.
name: phoenix-liveview-essentials description: "Use when writing LiveView modules or HEEx templates — mount/handle_params lifecycle, forms with to_form and phx-change, streams, assign_async/start_async, JS commands, hooks, function components."
> Verified against Elixir 1.20, Phoenix 1.8, LiveView 1.2 — October 2026
1. **`@impl true` before every callback** — `mount`, `handle_params`, `handle_event`, `handle_info`, `handle_async`, `render`. 2. **Pass `socket.assigns.current_scope` as the first argument to every context call** — `Blog.get_post!(scope, id)`, never `Blog.get_post!(id)`. Loading, updating or deleting by a client-supplied id without the scope is an IDOR. LiveViews call contexts, never `Repo`. 3. **Set every assign `render/1` reads before the first render** — static defaults in `mount/3`, URL-dependent ones (filter, sort, page) in `handle_params/3`. 4. **Subscribe and start timers in `mount/3` inside `if connected?(socket)`** — never in `handle_params/3`, which re-runs on every `push_patch` and stacks duplicate subscriptions. 5. **Load slow data with `assign_async`, `stream_async` or `start_async`** — not with `if connected?(socket)` branches or blocking queries in `mount/3`. 6. **Collections are streams** — `stream/4`, `stream_insert/4`, `stream_delete/3`, `stream(..., reset: true)` to refilter. The container needs an `id` and `phx-update="stream"`. 7. **Forms:** `to_form/2` into `@form`; `<.form for={@form} id="..." phx-change="validate" phx-submit="save">`; `<.button>` directly inside the form (`<.form>` declares only `inner_block` — an actions slot is silently dropped, so the button never renders). Validate with `to_form(changeset, action: :validate)`. 8. **Expected failures don't crash** — on `{:error, %Ecto.Changeset{}}` re-assign the form; let only bugs raise. 9. **Function components declare `attr`/`slot`**, with `default:` for optional attrs — never `Map.get(assigns, :x, default)` or `assigns[:x]`. 10. **HEEx:** `{@x}` in text and attributes, `:for`/`:if` attributes, `<%!-- --%>` comments; add `:key` to `:for` over plain lists. 11. **JS:** reach for `Phoenix.LiveView.JS` commands first, then colocated hooks; every `phx-hook` element needs a unique DOM `id`. 12. **Reuse `core_components.ex`** (`<.input>`, `<.button>`, `<.table>`, `<.header>`, `<.list>`, `<.icon>`) before writing new components.
Before finishing or reviewing a LiveView, walk through [the LiveView checklist](checklist.md). For hook callbacks, `push_event` payloads, `ColocatedJS`/`ColocatedCSS` setup and runtime hooks, read [the JS interop reference](js-interop.md).
Related skills: `phoenix-liveview-auth` (`live_session`, `on_mount` auth), `phoenix-pubsub-patterns` (broadcasting), `phoenix-uploads` (`allow_upload`, consuming entries), `testing-essentials` (LiveView tests), `phoenix-tailwind-daisyui` (Tailwind v4 `@source`, daisyUI themes, extending `core_components`).
---
HTTP GET mount/3 -> handle_params/3 -> render/1 connected?(socket) == false, static HTML WebSocket mount/3 -> handle_params/3 -> render/1 connected?(socket) == true, now live push_patch handle_params/3 -> render/1 same process; mount/3 does NOT run push_navigate new LiveView mounts over the same socket (same live_session); no page reload redirect/2 full HTTP page load
`mount/3` runs twice per page load, so synchronous work in it runs twice. Async tasks only start once connected — that's why `assign_async` replaces the old `if connected?` loading pattern. `push_navigate` to a route in a *different* `live_session` falls back to a full page load.
Based on `mix phx.gen.live` output (scoped context calls, subscription in `mount`, streams), plus a URL filter and single-item stream updates instead of the generator's re-fetch.
defmodule MyAppWeb.PostLive.Index do
use MyAppWeb, :live_view
alias MyApp.Blog
alias MyApp.Blog.Post
@impl true
def mount(_params, _session, socket) do
# Once per LiveView process. handle_params/3 re-runs on every push_patch.
if connected?(socket) do
Blog.subscribe_posts(socket.assigns.current_scope)
end
{:ok, assign(socket, :page_title, "Listing Posts")}
end
@impl true
def handle_params(params, _uri, socket) do
status = params["status"] || "all"
# list_posts/2 is the generated list_posts/1 plus a filter option
posts = Blog.list_posts(socket.assigns.current_scope, status: status)
{:noreply,
socket
|> assign(:status, status)
|> stream(:posts, posts, reset: true)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
scope = socket.assigns.current_scope
# Scoped lookup: another user's id raises Ecto.NoResultsError instead of deleting
post = Blog.get_post!(scope, id)
{:ok, _} = Blog.delete_post(scope, post)
{:noreply, stream_delete(socket, :posts, post)}
end
@impl true
def handle_info({:created, %Post{} = post}, socket) do
if visible?(post, socket.assigns.status) do
{:noreply, stream_insert(socket, :posts, post, at: 0)}
else
{:noreply, socket}
end
end
def handle_info({:updated, %Post{} = post}, socket) do
# Existing DOM id -> updated in place; an edit can also move it out of the filter
if visible?(post, socket.assigns.status) do
{:noreply, stream_insert(socket, :posts, post)}
else
{:noreply, stream_delete(socket, :posts, post)}
end
end
def handle_info({:deleted, %Post{} = post}, socket) do
{:noreply, stream_delete(socket, :posts, post)}
end
# Broadcasts carry every post in the scope; keep the stream matching the URL filter.
defp visible?(_post, "all"), do: true
defp visible?(%Post{status: status}, filter), do: to_string(status) == filter
end<Layouts.app flash={@flash} current_scope={@current_scope}>
<.link patch={~p"/posts?· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when preparing releases or deployment config — mix phx.gen.release, runtime.exs vs compile-time config, release migrations,…
Use when writing Ecto changeset validations beyond cast/validate_required — custom and cross-field validations (e.g. end date…
Use when defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes,…
Use when migrating PostgreSQL tables that serve live traffic — index concurrently,…
Use when one form or operation manages parent and child records together — cast_assoc/cast_embed, inputs_for with sort/drop…