
Liveview Code Review
- 74 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
Helps with ai & agent building tasks.
About
liveview-code-review is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- liveview-code-review
- AI & Agent Building
- AI-coding skill
Liveview Code Review by the numbers
- 74 all-time installs (skills.sh)
- Ranked #5,508 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill liveview-code-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 74 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
What it does
Helps with ai & agent building tasks.
Files
LiveView Code Review
Quick Reference
| Issue Type | Reference |
|---|---|
| mount, handle_params, handle_event, handle_async | references/lifecycle.md |
| When to use assigns vs streams, AsyncResult | references/assigns-streams.md |
| Function vs LiveComponent, slots, attrs | references/components.md |
| Authorization per event, phx-value trust | references/security.md |
Review Checklist
Critical Issues
- [ ] No socket copying into async functions (extract values first)
- [ ] Every handle_event validates authorization
- [ ] No sensitive data in assigns (visible in DOM)
- [ ] phx-value data is validated (user-modifiable)
Lifecycle
- [ ] Subscriptions wrapped in
connected?(socket) - [ ] handle_params used for URL-based state
- [ ] handle_async handles :loading and :error states
Data Management
- [ ] Streams used for large collections (100+ items)
- [ ] temporary_assigns for data not needed after render
- [ ] AsyncResult patterns for loading states
Components
- [ ] Function components preferred over LiveComponents
- [ ] LiveComponents preserve :inner_block in update/2
- [ ] Slots use proper attr declarations
- [ ] phx-debounce on text inputs
Valid Patterns (Do NOT Flag)
- Empty mount returning {:ok, socket} - Valid for simple LiveViews
- Using assigns for small lists - Streams only needed for 100+ items
- LiveComponent without update/2 - Default update/2 assigns all
- phx-click without phx-value - Event may not need data
- Inline function in heex - Valid for simple transforms
Context-Sensitive Rules
| Issue | Flag ONLY IF |
|---|---|
| Missing debounce | Input is text/textarea AND triggers server event |
| Use streams | Collection has 100+ items OR is paginated |
| Missing auth check | Event modifies data AND no auth in mount |
Critical Anti-Patterns
Socket Copying (MOST IMPORTANT)
# BAD - socket copied into async function
def handle_event("load", _, socket) do
Task.async(fn ->
user = socket.assigns.user # Socket copied!
fetch_data(user.id)
end)
{:noreply, socket}
end
# GOOD - extract values first
def handle_event("load", _, socket) do
user_id = socket.assigns.user.id
Task.async(fn ->
fetch_data(user_id) # Only primitive copied
end)
{:noreply, socket}
endMissing Authorization
# BAD - trusts phx-value without auth
def handle_event("delete", %{"id" => id}, socket) do
Posts.delete_post!(id) # Anyone can delete any post!
{:noreply, socket}
end
# GOOD - verify authorization
def handle_event("delete", %{"id" => id}, socket) do
post = Posts.get_post!(id)
if post.user_id == socket.assigns.current_user.id do
Posts.delete_post!(post)
{:noreply, stream_delete(socket, :posts, post)}
else
{:noreply, put_flash(socket, :error, "Unauthorized")}
end
endHard gates (sequence)
Advance only when each pass condition is objectively true (prevents reporting without evidence):
| Gate | Pass condition |
|---|---|
| G1 — Files in evidence | You have an explicit list of paths under review (e.g. *.ex, *.heex, or the paths the user named). Every finding names a file from that list. |
| G2 — Verification protocol | You loaded review-verification-protocol and applied its Pre-Report Verification (and issue-type sections where relevant) before treating something as a finding. |
| G3 — Line anchors | Each finding uses [FILE:LINE] where that line exists in the current file (confirmed by read/grep output, not inferred). |
| G4 — Valid-pattern screen | You checked the finding against Valid Patterns (Do NOT Flag) and Context-Sensitive Rules; if it matches a “do not flag” case or fails a “Flag ONLY IF,” you do not report it. |
Issue format
Use [FILE:LINE] ISSUE_TITLE for each finding.
Assigns and Streams
When to Use Each
| Use Case | Solution |
|---|---|
| Small list (< 100 items) | assigns |
| Large list (100+ items) | streams |
| Paginated/infinite scroll | streams |
| Data not needed after render | temporary_assigns |
| Async loading with states | AsyncResult |
Streams
Basic Usage
def mount(_params, _session, socket) do
{:ok, stream(socket, :posts, Posts.list_posts())}
end
def handle_event("delete", %{"id" => id}, socket) do
post = Posts.get_post!(id)
Posts.delete_post!(post)
{:noreply, stream_delete(socket, :posts, post)}
endIn Templates
<div id="posts" phx-update="stream">
<div :for={{dom_id, post} <- @streams.posts} id={dom_id}>
<%= post.title %>
<button phx-click="delete" phx-value-id={post.id}>Delete</button>
</div>
</div>Stream Operations
# Insert at end (default)
stream_insert(socket, :posts, new_post)
# Insert at beginning
stream_insert(socket, :posts, new_post, at: 0)
# Delete
stream_delete(socket, :posts, post)
# Delete by DOM ID
stream_delete_by_dom_id(socket, :posts, "posts-123")
# Reset entire stream
stream(socket, :posts, new_list, reset: true)AsyncResult
assign_async
def mount(_params, _session, socket) do
{:ok,
socket
|> assign_async(:user, fn ->
{:ok, %{user: Accounts.get_user!(1)}}
end)
|> assign_async([:posts, :comments], fn ->
{:ok, %{posts: Posts.list(), comments: Comments.list()}}
end)}
endTemplate Handling
<%# Using async_result component %>
<.async_result :let={user} assign={@user}>
<:loading>
<div class="animate-pulse">Loading...</div>
</:loading>
<:failed :let={{:error, reason}}>
<div class="text-red-500">Failed: <%= reason %></div>
</:failed>
<div><%= user.name %></div>
</.async_result>
<%# Manual pattern matching %>
<%= case @user do %>
<% %AsyncResult{ok?: true, result: user} -> %>
<%= user.name %>
<% %AsyncResult{loading: true} -> %>
Loading...
<% %AsyncResult{failed: reason} -> %>
Error: <%= inspect(reason) %>
<% end %>Temporary Assigns
For Large Rendered Data
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:messages, load_messages())
|> assign(:form, to_form(%{})),
temporary_assigns: [messages: []]}
endImportant: Temporary assigns are cleared after render. Only use for data that doesn't need to persist in socket state.
Common Mistakes
Assigns for Large Lists
# BAD - 10k items in assigns
def mount(_, _, socket) do
{:ok, assign(socket, items: Repo.all(Item))} # All 10k in memory!
end
# GOOD - stream with pagination
def mount(_, _, socket) do
{:ok,
socket
|> assign(:page, 1)
|> stream(:items, load_page(1))}
endNot Handling AsyncResult States
# BAD - assumes result exists
<%= @user.result.name %>
# GOOD - handle all states
<%= if @user.ok?, do: @user.result.name, else: "Loading..." %>Review Questions
1. Are streams used for large or paginated collections? 2. Do AsyncResult templates handle loading and error states? 3. Are temporary_assigns used appropriately (not for needed state)? 4. Is stream DOM properly configured (id, phx-update)?
LiveView Components
Function Components vs LiveComponents
Prefer Function Components
# GOOD - stateless, simple
defmodule MyAppWeb.Components do
use Phoenix.Component
attr :user, :map, required: true
def user_card(assigns) do
~H"""
<div class="card">
<h3><%= @user.name %></h3>
<p><%= @user.email %></p>
</div>
"""
end
endUse LiveComponent When Needed
Only use LiveComponent when you need:
- Component-local state
- Component-local event handling
- Lifecycle callbacks (mount, update)
defmodule MyAppWeb.LiveComponents.EditableField do
use MyAppWeb, :live_component
def mount(socket) do
{:ok, assign(socket, editing: false)}
end
def handle_event("toggle_edit", _, socket) do
{:noreply, assign(socket, editing: !socket.assigns.editing)}
end
def render(assigns) do
~H"""
<div phx-click="toggle_edit" phx-target={@myself}>
<%= if @editing do %>
<input value={@value} />
<% else %>
<%= @value %>
<% end %>
</div>
"""
end
endSlots
Basic Slots
slot :inner_block, required: true
def card(assigns) do
~H"""
<div class="card">
<%= render_slot(@inner_block) %>
</div>
"""
end
# Usage
<.card>
<p>Card content</p>
</.card>Named Slots
slot :header
slot :inner_block, required: true
slot :footer
def modal(assigns) do
~H"""
<div class="modal">
<header :if={@header != []}>
<%= render_slot(@header) %>
</header>
<main>
<%= render_slot(@inner_block) %>
</main>
<footer :if={@footer != []}>
<%= render_slot(@footer) %>
</footer>
</div>
"""
end
# Usage
<.modal>
<:header>Title</:header>
Main content
<:footer>
<button>Close</button>
</:footer>
</.modal>Slots with Arguments
slot :col, doc: "Table columns" do
attr :label, :string, required: true
end
attr :rows, :list, required: true
def table(assigns) do
~H"""
<table>
<thead>
<tr>
<th :for={col <- @col}><%= col.label %></th>
</tr>
</thead>
<tbody>
<tr :for={row <- @rows}>
<td :for={col <- @col}>
<%= render_slot(col, row) %>
</td>
</tr>
</tbody>
</table>
"""
end
# Usage
<.table rows={@users}>
<:col :let={user} label="Name"><%= user.name %></:col>
<:col :let={user} label="Email"><%= user.email %></:col>
</.table>LiveComponent Gotchas
Preserve inner_block in update/2
# BAD - loses inner_block
def update(assigns, socket) do
{:ok, assign(socket, field: assigns.field)}
end
# GOOD - preserve inner_block
def update(assigns, socket) do
{:ok,
socket
|> assign(:field, assigns.field)
|> assign(:inner_block, assigns[:inner_block])}
end
# BETTER - assign all and override
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign(:computed, compute(assigns.field))}
endTarget Events Correctly
# Event goes to LiveComponent
<button phx-click="save" phx-target={@myself}>Save</button>
# Event goes to parent LiveView
<button phx-click="close">Close</button>Review Questions
1. Are function components used for stateless UI? 2. Do LiveComponents actually need component-local state? 3. Are slots properly declared with attr/slot? 4. Is inner_block preserved in update/2?
LiveView Lifecycle
Mount
connected?/1 for Subscriptions
def mount(_params, _session, socket) do
# Only subscribe when actually connected (not during static render)
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "updates")
end
{:ok, assign(socket, items: [])}
endExpensive Operations
# BAD - blocks initial render
def mount(_params, _session, socket) do
items = Repo.all(Item) # Blocks!
{:ok, assign(socket, items: items)}
end
# GOOD - defer with assign_async
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Items")
|> assign_async(:items, fn -> {:ok, %{items: Repo.all(Item)}} end)}
endhandle_params
URL-Based State
def handle_params(%{"page" => page}, _uri, socket) do
page =
case Integer.parse(page) do
{n, ""} when n > 0 -> n
_ -> 1
end
{:noreply, assign(socket, page: page, items: load_page(page))}
end
def handle_params(_params, _uri, socket) do
{:noreply, assign(socket, page: 1, items: load_page(1))}
endLive Navigation
# Triggers handle_params, not full remount
<.link patch={~p"/items?page=2"}>Page 2</.link>
# Full remount (different LiveView)
<.link navigate={~p"/other"}>Other Page</.link>handle_event
Pattern Match Events
def handle_event("save", %{"form" => form_params}, socket) do
# Handle save
end
def handle_event("delete", %{"id" => id}, socket) do
# Handle delete
end
def handle_event("toggle", %{"value" => value}, socket) do
# Handle toggle
endForm Events
def handle_event("validate", %{"user" => params}, socket) do
changeset =
socket.assigns.user
|> User.changeset(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, changeset: changeset)}
end
def handle_event("save", %{"user" => params}, socket) do
case Accounts.update_user(socket.assigns.user, params) do
{:ok, user} ->
{:noreply,
socket
|> put_flash(:info, "Saved!")
|> push_navigate(to: ~p"/users/#{user}")}
{:error, changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
endhandle_async
With assign_async
def mount(_params, _session, socket) do
{:ok,
socket
|> assign_async(:user, fn -> {:ok, %{user: load_user()}} end)}
end
# In template - handle all states
<.async_result :let={user} assign={@user}>
<:loading>Loading user...</:loading>
<:failed :let={reason}>Error: <%= inspect(reason) %></:failed>
<%= user.name %>
</.async_result>With start_async
def handle_event("refresh", _, socket) do
{:noreply, start_async(socket, :refresh, fn -> fetch_data() end)}
end
def handle_async(:refresh, {:ok, data}, socket) do
{:noreply, assign(socket, data: data)}
end
def handle_async(:refresh, {:exit, reason}, socket) do
{:noreply, put_flash(socket, :error, "Refresh failed")}
endReview Questions
1. Are PubSub subscriptions wrapped in connected?(socket)? 2. Is handle_params used for URL-based state changes? 3. Do async operations handle both success and failure? 4. Is expensive loading deferred with assign_async?
LiveView Security
Event Authorization
Every Event Must Authorize
# BAD - trusts phx-value blindly
def handle_event("delete", %{"id" => id}, socket) do
Post.delete!(id) # Anyone can delete any post!
{:noreply, socket}
end
# GOOD - verify ownership
def handle_event("delete", %{"id" => id}, socket) do
post = Posts.get_post!(id)
if authorized?(socket.assigns.current_user, :delete, post) do
Posts.delete_post!(post)
{:noreply, stream_delete(socket, :posts, post)}
else
{:noreply, put_flash(socket, :error, "Not authorized")}
end
end
defp authorized?(user, :delete, post) do
user.id == post.user_id || user.admin
endDon't Trust phx-value
<%# This is user-modifiable in browser DevTools! %>
<button phx-click="edit" phx-value-id={@post.id} phx-value-role="admin">
Edit
</button>Always validate on server:
def handle_event("edit", %{"id" => id, "role" => _role}, socket) do
# Ignore client-provided role, check actual user
if socket.assigns.current_user.admin do
# Allow edit
end
endSensitive Data in Assigns
What Goes in Assigns is Visible
LiveView assigns can be inspected:
- In browser DevTools (morphdom payloads)
- In crash reports
- In logs
# BAD - sensitive data in assigns
socket
|> assign(:user, %{
email: "user@example.com",
password_hash: "...", # Sensitive!
api_token: "secret123" # Sensitive!
})
# GOOD - only needed fields
socket
|> assign(:user, %{
id: user.id,
name: user.name,
email: user.email
})Use Session for Sensitive State
# In mount, fetch from session
def mount(_params, session, socket) do
user_id = session["user_id"]
user = Accounts.get_user!(user_id)
{:ok, assign(socket, current_user: %{id: user.id, name: user.name})}
endInput Validation
Validate All User Input
def handle_event("update", %{"quantity" => qty}, socket) do
# BAD - no validation
{:noreply, assign(socket, quantity: String.to_integer(qty))}
end
def handle_event("update", %{"quantity" => qty}, socket) do
# GOOD - validate
case Integer.parse(qty) do
{n, ""} when n > 0 and n <= 100 ->
{:noreply, assign(socket, quantity: n)}
_ ->
{:noreply, put_flash(socket, :error, "Invalid quantity")}
end
endUse Changesets
def handle_event("save", %{"post" => params}, socket) do
changeset = Post.changeset(%Post{}, params)
if changeset.valid? do
# Proceed
else
{:noreply, assign(socket, changeset: changeset)}
end
endCSRF Protection
LiveView has built-in CSRF protection via the socket token. Ensure:
# In app.js
let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken} # Required!
})File Uploads
Validate File Types
allow_upload(socket, :avatar,
accept: ~w(.jpg .jpeg .png), # Whitelist extensions
max_file_size: 5_000_000, # 5MB limit
max_entries: 1
)Validate in consume_uploaded_entries
def handle_event("save", _, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{path: path}, entry ->
# Validate actual file content, not just extension
case ExImageInfo.info(File.read!(path)) do
{"image/jpeg", _, _} -> {:ok, save_file(path)}
{"image/png", _, _} -> {:ok, save_file(path)}
_ -> {:error, :invalid_file_type}
end
end)
endReview Questions
1. Does every handle_event validate authorization? 2. Is phx-value data treated as untrusted? 3. Are sensitive fields excluded from assigns? 4. Are file uploads validated by content, not just extension?