liveview-streams
Skill4 starsChanged 4 months agoMITView on GitHub
MANDATORY for handling large collections in LiveView. Invoke before rendering lists with 100+ items. Covers Phoenix.LiveView.stream/4, stream_insert, stream_delete, DOM patching efficiency, and pagination with streams. Available in LiveView 0.19+. Trigger words: stream, LiveView stream, large list, pagination, DOM patching, stream_insert,
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- apply-phoenix-controller-conventionsSkill
- ash-frameworkSkill
- benchee-profilingSkill
- broadway-data-pipelinesSkill
- cachex-cachingSkill
- code-qualitySkill
- code-reviewSkill
- deployment-gotchasSkill
- ecto-changeset-patternsSkill
- ecto-essentialsSkill
- ecto-migrationSkill
- ecto-nested-associationsSkill
- elixir-essentialsSkill
- gettext-i18nSkill
- mix-tasks-generatorsSkill
- oban-essentialsSkill
- otp-essentialsSkill
- phoenix-authenticationSkill
- phoenix-authorization-patternsSkill
- phoenix-channels-essentialsSkill
- phoenix-json-apiSkill
- phoenix-liveview-essentialsSkill
- phoenix-pubsub-patternsSkill
- phoenix-uploadsSkill
- property-based-testingSkill
- refactor-codeSkill
- req-http-clientSkill
- respond-to-reviewSkill
- security-essentialsSkill
- swoosh-emailsSkill
- telemetry-essentialsSkill
- testing-essentialsSkill
- typespec-dialyzerSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 35
Follow this sequence when implementing LiveView streams:
- Very specific: line 189
Use reset: true to clear existing items before inserting new ones — required for filtering and sorting:
- Distinctive: line 278
If stream patching is not working as expected, check for:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
LiveView Streams
RULES — Follow these with no exceptions
1. Use streams for collections with 100+ items; combine with pagination or infinite scroll — smaller lists use regular assigns; never stream unlimited items
2. Use stream_insert/3 and stream_delete/3 for incremental updates — never replace the entire stream assign
3. Use phx-update="stream" in templates — required for stream DOM patching; missing this causes full re-renders
4. Use reset: true for filtering and sorting — clears existing items before inserting the new result set
5. Call stream_configure/3 before stream/3 — configuring a custom dom_id after the stream is initialized has no effect
6. Re-fetch from the database when sorting or filtering — streams do not keep items in assigns, so you cannot reorder them in memory
End-to-End Workflow
Follow this sequence when implementing LiveView streams:
- Identify collection size — see Rule 1 above for the threshold
- Define DOM ID strategy — decide on
idattribute format (e.g.,post-#{post.id}) - Add
stream_configure/3inmount/3to set custom DOM ID generator - Initialize stream — call
stream(socket, :name, collection)inmount/3 - Update template — add
phx-update="stream"container andid={dom_id}on items - Handle events — use
stream_insert/stream_deletefor all mutations - Add handle_info — handle Phoenix.PubSub broadcasts with stream updates
- Verify WS frames — confirm targeted DOM patches in browser DevTools
Basic Stream Pattern
defmodule MyAppWeb.PostLive.Index do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> stream_configure(:posts, dom_id: &"post-#{&1.id}")
|> stream(:posts, Blog.list_posts())}
end
@impl true
def handle_event("create", %{"post" => params}, socket) do
case Blog.create_post(params) do
{:ok, post} ->
{:noreply, stream_insert(socket, :posts, post, at: 0)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
end
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
with {:ok, post} <- Blog.fetch_post(id),
{:ok, _} <- Blog.delete_post(post) do
{:noreply, stream_delete(socket, :posts, post)}
else
{:error, :not_found} ->
{:noreply, put_flash(socket, :error, "Post not found")}
{:error, %Ecto.Changeset{}} ->
{:noreply, put_flash(socket, :error, "Could not delete post")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Could not delete post")}
end
end
@impl true
def handle_info({:post_created, post}, socket) do
{:noreply, stream_insert(socket, :posts, post, at: 0)}
end
endTemplate with Streams
<div id="posts" phx-update="stream">
<div :for={{dom_id, post} <- @streams.posts} id={dom_id} class="post">
<h3><%= post.title %></h3>
<p><%= post.body %></p>
<button phx-click="delete" phx-value-id={post.id}>
Delete
</button>
</div>
</div>Infinite Scroll with Streams
@per_page 20
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page, 1)
|> assign(:loading, false)
|> stream_configure(:posts, dom_id: &"post-#{&1.id}")
|> stream(:posts, Blog.list_posts(page: 1, per_page: @per_page))}
end
@impl true
def handle_event("load_more", _params, socket) do
page = socket.assigns.page + 1
new_posts = Blog.list_posts(page: page, per_page: @per_page)
{:noreply,
socket
|> assign(:page, page)
|> then(fn s -> Enum.reduce(new_posts, s, &stream_insert(&2, :posts, &1)) end)}
end<div id="posts" phx-update="stream">
<div :for={{dom_id, post} <- @streams.posts} id={dom_id}>
<%= post.title %>
</div>
</div>
<div phx-hook="InfiniteScroll" data-page={@page}>
<button phx-click="load_more" disabled={@loading}>Load More</button>
</div>PubSub Integration with Streams
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "posts")
end
{:ok,
socket
|> stream_configure(:posts, dom_id: &"post-#{&1.id}")
|> assign(:editing_id, nil)
|> assign(:form, nil)
|> stream(:posts, Blog.list_posts())}
end
@impl true
def handle_info({:post_created, post}, socket) do
{:noreply, stream_insert(socket, :posts, post, at: 0)}
endBroadcast from your context:
def create_post(attrs) do
{:ok, post} = Repo.insert(Post.changeset(%Post{}, attrs))
Phoenix.PubSub.broadcast(MyApp.PubSub, "posts", {:post_created, post})
{:ok, post}
endResetting and Replacing Streams
Use reset: true to clear existing items before inserting new ones — required for filtering and sorting:
# Filtering
def handle_event("filter", %{"status" => status}, socket) do
{:noreply, stream(socket, :posts, Blog.list_posts_by_status(status), reset: true)}
end
# Sorting — re-fetch from the database because streams do not store items in assigns
def handle_event("sort", %{"column" => col, "direction" => dir}, socket) do
direction = String.to_existing_atom(dir)
sorted = Blog.list_posts(order_by: [{direction, String.to_existing_atom(col)}])
{:noreply,
socket
|> assign(:sort_direction, direction)
|> stream(:posts, sorted, reset: true)}
endEdit-in-Place Pattern
@impl true
def handle_event("start_edit", %{"id" => id}, socket) do
case Blog.fetch_post(id) do
{:ok, post} ->
{:noreply,
socket
|> assign(:editing_id, id)
|> assign(:form, to_form(Blog.change_post(post)))}
{:error, :not_found} ->
{:noreply, put_flash(socket, :error, "Post not found")}
end
end
@impl true
def handle_event("save_edit", %{"id" => id, "post" => params}, socket) do
with {:ok, post} <- Blog.fetch_post(id),
{:ok, updated_post} <- Blog.update_post(post, params) do
{:noreply,
socket
|> assign(:editing_id, nil)
|> assign(:form, nil)
|> stream_insert(:posts, updated_post)}
else
{:error, :not_found} ->
{:noreply, put_flash(socket, :error, "Post not found")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply,
socket
|> assign(:editing_id, id)
|> assign(:form, to_form(changeset))}
end
end
@impl true
def handle_event("cancel_edit", _params, socket) do
{:noreply,
socket
|> assign(:editing_id, nil)
|> assign(:form, nil)}
end<div id="posts" phx-update="stream">
<div :for={{dom_id, post} <- @streams.posts} id={dom_id}>
<%= if @editing_id == post.id do %>
<.form for={@form} phx-submit="save_edit">
<input type="hidden" name="id" value={post.id} />
<.input field={@form[:title]} type="text" label="Title" />
<button type="submit">Save</button>
<button type="button" phx-click="cancel_edit">Cancel</button>
</.form>
<% else %>
<h3><%= post.title %></h3>
<button phx-click="start_edit" phx-value-id={post.id}>Edit</button>
<% end %>
</div>
</div>Debugging Checklist
If stream patching is not working as expected, check for:
- Missing
phx-update="stream"on the container<div> - Missing
id={dom_id}on each item element - Accidentally replacing the entire stream assign instead of using
stream_insert/stream_delete - DOM ID collisions caused by non-unique item IDs (especially across pages in infinite scroll)
- Using
stream_insertwithout properdom_idconfiguration - Forgetting to call
stream_configurebeforestreamwhen using custom IDs
Manual verification step: Open your browser's DevTools panel → Network tab → WS frames. Confirm incoming frames contain targeted patch operations for individual items, not full list replacements. If you see full re-renders, the first two checklist items above are the most common cause.
Common Pitfalls
| ❌ Don't | ✅ Do |
| Replace the whole stream assign to update it | Use stream_insert/3 and stream_delete/3 for incremental changes |
Omit phx-update="stream" on the container | Add phx-update="stream" so LiveView patches items instead of re-rendering |
Forget id={dom_id} on each item element | Set id={dom_id} so DOM patching can target individual rows |
| Reuse non-unique IDs across pages in infinite scroll | Configure a stable dom_id (e.g. &"post-#{&1.id}") so IDs never collide |
| Sort or filter from items held in assigns | Re-fetch from the database and apply stream(..., reset: true) |
| Stream tiny collections (a handful of rows) | Use regular assigns; reserve streams for 100+ item lists |
Call stream_configure/3 after stream/3 | Configure the custom dom_id before the stream is initialized |
Integration
| Predecessor | This Skill | Successor |
| phoenix-liveview-essentials | liveview-streams | phoenix-pubsub-patterns |
| ecto-essentials | liveview-streams | testing-essentials |
Companion skills: phoenix-liveview-essentials, phoenix-pubsub-patterns, testing-essentials.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

