Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
Advisory
Suggest reviewing before use
Use this skill when writing new LiveView modules or modifying existing LiveView code to ensure consistent, idiomatic Phoenix patterns.
Precondition: Invoke phoenix-liveview-essentials before this skill for the full callback lifecycle reference.
@impl true before every callback (mount/3, handle_event/3, handle_info/2, handle_params/3, render/1)mount/3 — the disconnected render must never raise KeyErrorif connected?(socket) — PubSub subscriptions, timers, and async work run only when the WebSocket is connected{:noreply, socket} from handle_event/3 and handle_info/2 — never {:reply, ...} unless replying to a client pushput_flash or changeset assigns — never raise inside a callbackdef, not defp — private components cannot be used across templateswith for 2+ sequential fallible operations instead of nested caseRepo directly from a LiveView — delegate to context functionsmount/3 with safe defaults — initialize all assigns so disconnected render never raises a KeyErrorhandle_params/3 for URL-dependent assigns — guard any PubSub subscriptions with connected?(socket)handle_event/handle_info callbacks — one @impl true per callback; use with for 2+ fallible stepsdef, not defpput_flash or changeset assigns; never raise| Pattern | Convention |
|---|---|
@impl true | Before every callback (mount, handle_event, handle_info, handle_params, render) |
| Assigns in mount | Static defaults in mount; URL-dependent in handle_params |
| Side effects | Guard with if connected?(socket) — only run when WebSocket connected |
| Return value | {:noreply, socket} from handle_event/handle_info |
| Error handling | Assign errors to socket with put_flash; never raise |
| Components | Use def (exported), not defp |
| Multi-step errors | Use with instead of nested case |
| Database access | Call context functions only — never query Repo directly from a LiveView |
LiveView renders twice per page load:
| Phase | Request | connected?(socket) | Side effects |
|---|---|---|---|
| Disconnected | HTTP | false | No PubSub, no timers |
| Connected | WebSocket | true | PubSub, timers, async work |
Always initialize assigns to safe defaults in Phase 1 so the static HTML never raises a KeyError before WebSocket connects.
✅ Good — @impl true, safe defaults, guarded side effects:
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:user, nil)
|> assign(:loading, false)
|> assign(:notifications, [])
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "notifications")
end
{:ok, socket}
endCheckpoint: Static HTML must render without KeyError before WebSocket connects.
✅ Good — @impl true, {:noreply, socket}, assign errors to socket:
@impl true
def handle_event("delete", %{"id" => id}, socket) do
case Posts.delete_post(id) do
{:ok, _post} ->
{:noreply, assign(socket, :posts, Posts.list_posts())}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, "Could not delete post")}
end
end❌ Bad — nested case for 2+ fallible operations:
def handle_event("process", %{"id" => id}, socket) do
case Items.get_item(id) do
{:ok, item} ->
case Items.process(item) do
{:ok, result} -> {:noreply, assign(socket, :result, result)}
{:error, reason} -> {:noreply, put_flash(socket, :error, "Failed")}
end
{:error, :not_found} -> {:noreply, put_flash(socket, :error, "Not found")}
end
end✅ Good — with for 2+ sequential fallible operations:
@impl true
def handle_event("process", %{"id" => id}, socket) do
with {:ok, item} <- Items.get_item(id),
{:ok, result} <- Items.process(item) do
{:noreply, assign(socket, :result, result)}
else
{:error, :not_found} ->
{:noreply, put_flash(socket, :error, "Item not found")}
{:error, reason} ->
{:noreply, put_flash(socket, :error, "Processing failed: #{inspect(reason)}")}
end
end✅ Good — @impl true, use update for immutable assign changes:
@impl true
def handle_info({:item_updated, item}, socket) do
{:noreply, update(socket, :items, fn items -> [item | items] end)}
end
@impl true
def handle_info(%{event: "presence_diff"}, socket) do
{:noreply, assign(socket, :online_count, Presence.count())}
end❌ Bad — no @impl true, side effects not guarded, wrong return tuple:
def handle_params(%{"id" => id}, _uri, socket) do
post = Posts.get_post!(id)
Phoenix.PubSub.subscribe(MyApp.PubSub, "post:#{id}")
{:reply, %{post: post}, assign(socket, :post, post)}
end✅ Good — @impl true, guard connected? for subscriptions, return {:noreply, socket}:
@impl true
def handle_params(%{"id" => id}, _uri, socket) do
post = Posts.get_post!(id)
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "post:#{id}")
end
{:noreply, assign(socket, :post, post)}
end
@impl true
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end❌ Bad — defp makes the component unexportable and unusable across templates:
defmodule MyAppWeb.Components do
use Phoenix.Component
defp card(assigns) do
~H"""
<div class="card"><h3><%= @title %></h3></div>
"""
end
end✅ Good — def (exported), reusable across templates:
defmodule MyAppWeb.Components do
use Phoenix.Component
def card(assigns) do
~H"""
<div class="card">
<h3><%= @title %></h3>
<p><%= @content %></p>
</div>
"""
end
def badge(assigns) do
~H"""
<span class={"badge badge-#{@variant}"}><%= @label %></span>
"""
end
endUsage in templates:
<.card title="Hello" content="World" />
<.badge variant="success" label="Active" />❌ Bad — hardcoded children instead of slots:
def modal(assigns) do
~H"""
<div class="modal">
<header><%= @title %></header>
<div>Are you sure?</div>
<button phx-click="cancel">Cancel</button>
</div>
"""
end✅ Good — use render_slot for flexible content:
def modal(assigns) do
~H"""
<div class="modal">
<header><%= @title %></header>
<%= render_slot(@inner_block) %>
<footer><%= render_slot(@footer) %></footer>
</div>
"""
endUsage:
<.modal title="Confirm">
Are you sure?
<:footer>
<button phx-click="cancel">Cancel</button>
</:footer>
</.modal>✅ Good — @impl true, separate validate event, assign changeset on error:
@impl true
def mount(_params, _session, socket) do
changeset = Post.changeset(%Post{}, %{})
{:ok, assign(socket, form: to_form(changeset))}
end
@impl true
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
@impl true
def handle_event("save", %{"post" => params}, socket) do
case Posts.create_post(params) do
{:ok, _post} ->
{:noreply, put_flash(socket, :info, "Created!")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end<.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>✅ Good — use assign/update for immutable changes:
# Single assign
socket = assign(socket, :count, 0)
# Multiple assigns
socket = assign(socket, count: 0, name: "User", active: true)
# Update existing
socket = update(socket, :count, &(&1 + 1))In render/1 — direct access is safe when initialized in mount:
@impl true
def render(assigns) do
~H"""<p>Count: <%= @count %></p>"""
endIn helper functions — use Map.get for optional assigns:
defp format_user(socket) do
case Map.get(socket.assigns, :current_user) do
nil -> "Guest"
user -> user.name
end
end| ❌ Don't | ✅ Do |
|---|---|
Access an assign in render/1 that mount/3 never set | Initialize every assign in mount/3 before the first render |
Subscribe to PubSub unconditionally in mount/3 | Wrap subscriptions in if connected?(socket) |
Omit @impl true on callbacks | Add @impl true above every callback |
Define a reusable component with defp | Export it with def so templates can call it |
Nest case for multi-step operations | Use with and handle failures in else |
raise on a failed operation inside a callback | Assign the error to the socket via put_flash |
Call Repo directly inside a LiveView | Delegate to a context function |
| Predecessor | This Skill | Successor |
|---|---|---|
| elixir-essentials | apply-phoenix-liveview-conventions | code-quality |
| phoenix-liveview-essentials | apply-phoenix-liveview-conventions | testing-essentials |
Companion skills:
phoenix-liveview-essentials — deep LiveView callback lifecycle referenceliveview-streams — large collection rendering (100+ items)phoenix-pubsub-patterns — PubSub subscription managementphoenix-liveview-auth — authentication in LiveViews.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators