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} end
Missing 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 end
Before Submitting Findings
Use the issue format: [FILE:LINE] ISSUE_TITLE for each finding.
Load and follow review-verification-protocol before reporting any issue.