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
Lifecycle
Data Management
Components
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.
1---2name: liveview-code-review3description: Reviews Phoenix LiveView code for lifecycle patterns, assigns/streams usage, components, and security. Use when reviewing LiveView modules, .heex templates, or LiveComponents.4---5
6# LiveView Code Review
7
8## Quick Reference
9
10| Issue Type | Reference |
11|------------|-----------|
12| mount, handle_params, handle_event, handle_async | [references/lifecycle.md](references/lifecycle.md) |
13| When to use assigns vs streams, AsyncResult | [references/assigns-streams.md](references/assigns-streams.md) |
14| Function vs LiveComponent, slots, attrs | [references/components.md](references/components.md) |
15| Authorization per event, phx-value trust | [references/security.md](references/security.md) |
16
17## Review Checklist
18
19### Critical Issues
20- [ ] No socket copying into async functions (extract values first)
21- [ ] Every handle_event validates authorization
22- [ ] No sensitive data in assigns (visible in DOM)
23- [ ] phx-value data is validated (user-modifiable)
24
25### Lifecycle
26- [ ] Subscriptions wrapped in `connected?(socket)`
27- [ ] handle_params used for URL-based state
28- [ ] handle_async handles :loading and :error states
29
30### Data Management
31- [ ] Streams used for large collections (100+ items)
32- [ ] temporary_assigns for data not needed after render
33- [ ] AsyncResult patterns for loading states
34
35### Components
36- [ ] Function components preferred over LiveComponents
37- [ ] LiveComponents preserve :inner_block in update/2
38- [ ] Slots use proper attr declarations
39- [ ] phx-debounce on text inputs
40
41## Valid Patterns (Do NOT Flag)
42
43- **Empty mount returning {:ok, socket}** - Valid for simple LiveViews
44- **Using assigns for small lists** - Streams only needed for 100+ items
45- **LiveComponent without update/2** - Default update/2 assigns all
46- **phx-click without phx-value** - Event may not need data
47- **Inline function in heex** - Valid for simple transforms
48
49## Context-Sensitive Rules
50
51| Issue | Flag ONLY IF |
52|-------|--------------|
53| Missing debounce | Input is text/textarea AND triggers server event |
54| Use streams | Collection has 100+ items OR is paginated |
55| Missing auth check | Event modifies data AND no auth in mount |
56
57## Critical Anti-Patterns
58
59### Socket Copying (MOST IMPORTANT)
60
61```elixir
62# BAD - socket copied into async function
63def handle_event("load", _, socket) do
64 Task.async(fn ->
65 user = socket.assigns.user # Socket copied!
66 fetch_data(user.id)
67 end)
68 {:noreply, socket}
69end
70
71# GOOD - extract values first
72def handle_event("load", _, socket) do
73 user_id = socket.assigns.user.id
74 Task.async(fn ->
75 fetch_data(user_id) # Only primitive copied
76 end)
77 {:noreply, socket}
78end
79```
80
81### Missing Authorization
82
83```elixir
84# BAD - trusts phx-value without auth
85def handle_event("delete", %{"id" => id}, socket) do
86 Posts.delete_post!(id) # Anyone can delete any post!
87 {:noreply, socket}
88end
89
90# GOOD - verify authorization
91def handle_event("delete", %{"id" => id}, socket) do
92 post = Posts.get_post!(id)
93
94 if post.user_id == socket.assigns.current_user.id do
95 Posts.delete_post!(post)
96 {:noreply, stream_delete(socket, :posts, post)}
97 else
98 {:noreply, put_flash(socket, :error, "Unauthorized")}
99 end
100end
101```
102
103## Before Submitting Findings
104
105Use the issue format: `[FILE:LINE] ISSUE_TITLE` for each finding.
106
107Load and follow [review-verification-protocol](../review-verification-protocol/SKILL.md) before reporting any issue.