I have an application that renders hundreds of individual entities, and occasionally updates a few of them based on user interaction. I’m having a very hard time getting LiveView to only send the data for those entities that have changed. I’ve boiled down what I don’t understand to this very minimal example.
defmodule TestLiveViewUpdatesWeb.HomeLive do
use TestLiveViewUpdatesWeb, :live_view
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(
elements: ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"]
)}
end
def render(assigns) do
~H"""
<div>
<ul>
<li :for={element <- @elements}><%= element %></li>
</ul>
<br /><br />
<button phx-click="change_four">Change Four</button>
</div>
"""
end
def handle_event("change_four", _params, socket) do
elements = List.replace_at(socket.assigns.elements, 3, "#{:rand.uniform(1000)}")
{:noreply, assign(socket, elements: elements)}
end
end
This code renders the list elements, and when the user clicks the button it updates just the “four” to a random number. When I look in the message that is sent when I click the button, I see this.
It re-sends the values for all the elements, not just the fourth. I was under the impression that LiveView would send only what has changed. What am I not understanding here? Is there some way I can change the code to recognize that the other elements of the list have not changed, and not send them over the network?
In my larger app this results in 200kb being sent over the wire, when just a few bytes could represent what has changed.
LiveView does not perform diffing inside lists. Therefore, if you change one item in the list, from LiveView’s view the whole list is changed and therefore the for is evaluated again. The recommended approach to handle this is streams, which also have the advantage of not taking up memory on the server.
There is another way to do it, if you need to keep state on the server: LiveComponents. If you refactor the list items to be rendered in their own LiveComponents, the diff will be minimized as well:
defmodule TestLiveViewUpdatesWeb.ListComponent do
use TestLiveViewUpdatesWeb, :live_component
def render(assigns) do
~H"""
<li><%= @element %></li>
"""
end
end
defmodule TestLiveViewUpdatesWeb.HomeLive do
use TestLiveViewUpdatesWeb, :live_view
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(
elements: ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"]
)}
end
def render(assigns) do
~H"""
<div>
<ul>
<.live_component id={element} module={TestLiveViewUpdatesWeb.ListComponent} :for={element <- @elements} element={element} />
</ul>
<br /><br />
<button phx-click="change_four">Change Four</button>
</div>
"""
end
def handle_event("change_four", _params, socket) do
elements = List.replace_at(socket.assigns.elements, 3, "#{:rand.uniform(1000)}")
{:noreply, assign(socket, elements: elements)}
end
end
Thanks. After reading those docs, I can see this is the way to go for my use case. However, the pieces (yes this is a game app) that will change based on game logic isn’t occurring in a way that it will be easy to hook the various stream/4 calls in where changes, inserts, and deletes to that list will occur.
What I’m going to try is to use the myers_difference/2 differ to generate a sequence of stream/4 and stream_delete/3 to update the minimal number of elements in the stream. I’ll post back here if I succeed.
I implemented this function to both store the list of game units on the socket, and to update the stream w/ deletes and inserts on changes. It works well. It cut down the network traffic by 10x.
def stream_ui_units(socket, ui_units) do
if Map.has_key?(socket.assigns, :ui_units) do
delta = List.myers_difference(socket.assigns.ui_units, ui_units)
Enum.reduce(delta, socket, fn {op, ui_units}, socket ->
case op do
:eq ->
socket
:ins ->
Enum.reduce(ui_units, socket, fn ui_unit, socket ->
stream_insert(socket, :ui_units, ui_unit)
end)
:del ->
Enum.reduce(ui_units, socket, fn ui_unit, socket ->
stream_delete(socket, :ui_units, ui_unit)
end)
end
end)
else
stream(socket, :ui_units, ui_units, reset: true)
end
|> assign(ui_units: ui_units)
end
I realize this does not take advantage of streams’ memory saving nature, as I store the whole list on the socket anyway. But, I’m OK with that in my app.