The mapping between the state and the rendered template is not one-to-one in any nontrivial case. I think it’s best if I illustrate this in code.
This mapping is one-to-one: one message, one div. If you use streams for this, you can replace the messages with a stream and do a stream_insert/4 whenever a message is updated. This is the canonical use-case for streams, and it will work fine.
assign(socket, :messages, [%Message{...}, %Message{...}, ...])
defp add(socket, message) do
assign(socket, :messages, socket.assigns.messages ++ [message])
end
# OR
stream(socket, :messages, [%Message{...}, ...])
defp add(socket, message) do
stream_insert(socket, :messages, message)
end
~H"""
<div :for={msg <- @messages}>{msg.text}</div>
"""
But imagine we change the template to include a “message count” somewhere in the UI.
The mapping between %Message{}s and the template is no longer one-to-one. There are parts of the state (the number of messages) which are an arbitrary function (in this case a count) of the messages.
~H"""
<div>You have {length(@messages)} messages!</div>
<div :for{msg <- @messages}>{msg.text}</div>
"""
The issue with using streams for this is that when you try to use stream_insert, it only knows how to update a single message, but it doesn’t understand that it also has to update the count. So you would have to write what I’ve been calling “special-case code” to handle that specific case. Something like:
defp add(socket, message) do
if message_is_new?(socket, message) do
socket
|> stream_insert(:messages, message)
|> assign(:message_count, socket.assigns.message_count + 1)
else
stream_insert(socket, :messages, message)
end
end
In the case of our little example the special-case code is still rather simple, but as we add more features the special cases will start to increase exponentially as they will all have weird dependencies on each other. That’s what the article I linked you was about.
So what does this have to do with recursive data?
Well, in the example I gave (the expand/collapse node), we want to render the button conditionally depending on whether a folder has children or not. The problem is that, when we update a child, the stream will not, on its own, understand that it has to update the parent. So we would have to write some sort of special case.
Note that this is pseudo-code, and the real code would be more complicated.
defp update_node(socket, node) do
old_parent = get_old_parent_node(node)
new_parent = get_new_parent_node(node)
if length(old_parent.children) == 1 do
# old parent will now be empty
stream_insert(socket, :nodes, %Node{old_parent | ...})
else
socket
end
# same for new parent, and so on...
end
And then you have to write more code like this for many other features.
However, if you avoid streams and just write this as a normal LiveView, you will not have this problem. This is why I advised you to be wary of streams for this use-case.






















