Props & data
Phoenix Streams
A stream assign arrives in React as an array with __dom_id
- stream/4
- __dom_id
Key concepts
stream/4 manages a large or growing collection without keeping
every item in LiveView memory or re-sending the whole list on every
change. Assigning a stream to a prop hands React the current list
as a plain array โ inserts, updates, deletes and resets all arrive
as ordinary prop diffs.
defmodule MyAppWeb.StreamsLive do
use MyAppWeb, :live_view
@chatter [
"turns out it was DNS. it's always DNS.",
"works on my machine ๐คท",
"the bug fixed itself. I don't trust it.",
"coffee count: 4. regrets: 0.",
"ship it ๐ข"
]
def mount(_params, _session, socket) do
socket =
socket
|> assign(:next_id, 1)
|> stream(:messages, [%{id: 0, text: "hey, anyone here? ๐"}])
{:ok, socket}
end
def render(assigns) do
~H"""
<.react name="Streams" messages={@streams.messages} socket={@socket} />
"""
end
# A brand new message is appended to the stream.
def handle_event("add", params, socket) do
id = socket.assigns.next_id
socket =
socket
|> assign(:next_id, id + 1)
|> stream_insert(:messages, %{id: id, text: message_text(params)})
{:noreply, socket}
end
# `update_only: true` patches a message already on the page, without
# moving it or re-inserting it if it's gone.
def handle_event("edit", %{"id" => id} = params, socket) do
message = %{id: id, text: message_text(params)}
{:noreply, stream_insert(socket, :messages, message, update_only: true)}
end
def handle_event("delete", %{"id" => id}, socket) do
{:noreply, stream_delete(socket, :messages, %{id: id})}
end
# `reset: true` throws the whole conversation away and starts a new one.
def handle_event("replace_all", _params, socket) do
id = socket.assigns.next_id
messages =
@chatter
|> Enum.shuffle()
|> Enum.take(3)
|> Enum.with_index(id)
|> Enum.map(fn {text, index} -> %{id: index, text: text} end)
socket =
socket
|> assign(:next_id, id + length(messages))
|> stream(:messages, messages, reset: true)
{:noreply, socket}
end
defp message_text(%{"text" => text}) do
case String.trim(text) do
"" -> Enum.random(@chatter)
text -> text
end
end
defp message_text(_params), do: Enum.random(@chatter)
endHow it works
Every item in a stream carries a __dom_id
key that
LiveReact adds โ a stable id derived from Phoenix's own stream ref,
not from any field of the message itself. Use it, not message.id, as the React key: it stays
correct across stream_insert/3, stream_delete/3
and a full reset: true
replacement, which is exactly what
a stream's own DOM-patching semantics guarantee on the server side.
stream_insert(socket, :messages, message, update_only: true)
patches an item that's already on the page without moving or
re-inserting it โ that's what backs the "Edit" button, in contrast
to a plain insert for "Send" and a full reset: true
for "Replace all".