LiveView change diff pushing too much data

Hello friends,

I’ve got the following snippet on a .heex template in my LiveView for rendering a board for a game:

<div class='board'>
  <%= for y <- 1..100 do %>
    <%= for x <- 1..100 do %>
      <div   class={Map.get(@rendered_board, {x,y})}></div>
    <% end  %>
  <% end  %>
</div>

The rendered_board assign is a Map with keys from {1,1} to {100,100} and values like :empty, :dirty, :player etc. The mount function of my liveview initializes the rendered_board assign and there’s an event handler that is executed when the player is moved. So this event handler will change like 1 element (i.e the player moved 1 square up). So the return of the event handler will be something like:

{:noreply, socket |> Map.put(socket.assigns.rendered_board, {1,1}, :new_value)}

Notice that the rendered_board assign is changed but only 1 element of that assign is really changed.

My problem is that when that happens, the liveview pushes all class elements to my socket (not only the changed ones) ! This results in a payload of like 162kb. The payload is similar to this:

["4","268","lv:phx-Fr94ffyVaEgkhA4j","phx_reply",{"response":{"diff":{"2":{"0":{"0":"{86, 43}","2":"%{1 =&gt; %{pos: {86, 43}, socket_id: &quot;phx-Fr94ffyVaEgkhA4j&quot;}}",
"3":{"d":[[{"d":[["square empty"],["square dirt"],["square dirt"],["square dirt"],["square dirt"],
["square dirt"],["square dirt"],["square dirt"],["square dirt"]
...
thousands more elements
...
,["square dirt"],["square dirt"]],"s":0}]],"p":{"0":["\n      <div class=\"","\"></div>\n"]}}}}}},"status":"ok"}]

Is there a way to have the correct behavior ? I.e I wanted live view to push only the changed elements and leave the others alone. I tried assigning ids to the board divs but no luck. Is what I want even possible with LiveView ? If it ain’t then I could try a client-side solution but I’d rather avoid JS :slight_smile:

Kind regards,
Serafeim

I might be wrong, but I believe this is because of immutable data structures. Map.put is returning a new Map, not just updating the previous Map. I think LiveView is therefore acting as intended. Under the hood Elixir’s Map.put calls Erlang :maps.put/3. From the documentation of that function:

Associates Key with value Value and inserts the association into map Map2. If key Key already exists in map Map1, the old associated value is replaced by value Value. The function returns a new map Map2 containing the new association and the old associations in Map1.

Emphasis mine.

1 Like

Yes, this is expected. We can’t do diff tracking inside a list. So the whole list is always rendered. There are two options:

  1. Add phx-update="append" to <div class="board">, mark @rendered_board as a temporary assign and generate a DOM ID for each element in the @rendered_board. Now, you will only store in the @rendered_board something you want to change. When you render something with an existing ID, LiveView will update it in place and the phx-update="append" won’t let the previous elements be discarded.

  2. Put each board element inside a LiveComponent and then you have the LiveComponent update its own state.

The best solution is going to depend on the application and the amount of data. In your case, 1 sounds like a saner bet.

We also want to introduce the idea of collections in the future, which hopefully will make some of those cases simpler.

4 Likes

Hello @josevalim thank you so much for the answer but unfortunately I can’t make it work!

I changed my template like this:

<div id='board' class='board' phx-update="append" >
  <%= for y <- 1..100 do %>
    <%= for x <- 1..100 do %>
      <div  id={"#{x}_#{y}"}  class={Map.get(@rendered_board, {x,y})}></div>
    <% end  %>
  <% end  %>
</div>

and then in mount I initialized the temporary_assigns like

socket =
        socket |>
        assign(:connected, true) |>
        assign(:pos, pos) |>
        assign(:player_id, player_id) |>
        assign(:players, players) |>
        assign(:rendered_board, rendered_board)

      {:ok, socket, temporary_assigns: [rendered_board: %{}]}

Now, when a player moves I assign only the new and old position of the player on the rendered board (i. the rendered_board has 2 elements instead of 10 000):

socket = socket
    |> assign(:rendered_board, rendered_board)
    |> assign(:players, players)
    |> assign(:pos, Map.get(players, player_id) |> Map.get(:pos))

    {:noreply, socket}

The problem is that when this gets rendered again after the player move it will display only the changes and lose anything else the board has! I.e the Map.get() inside for y for x loop will return values onlly for the changes so my board will display only two squares… I tried doing update for the rendered_board instead of assigns but no luck :frowning:

Any help will be appreaciated! Also if I get a definite answer that it can’t be done I’ll also be happy since I’ll be able to move on and try doing it client-side with alpine js.

Thank you and kind regards,
Serafeim

The above should work. I wonder if it is related to your ids? An ID cannot be start with a number, try {"p#{x}_#{y}"}.

1 Like

No, it doesn’t work even with the new ids :expressionless:

What I’m confused about is that since I’m assigning a rendered_board with only a couple of elements after a player moevs, won’t the Map.get(@rendered_board, {x,y}) return nil for all elements in the for loop ? I mean I understand that the temporary_assigns is discarded ?

Also, this may be useful but when I take a peek at the message I receive it still is wa too large (like 150k) and it contains someting like:

["4","20","lv:phx-Fr-wKewGGvx9rwNi","phx_reply",{"response":{"diff":{"2":{"0":{"0":"{4, 8}","2":"%{1 =&gt; %{pos: {4, 8}, socket_id: &quot;phx-Fr-wKewGGvx9rwNi&quot;}}","3":{"d":[[{"d":[["1","1","square dirt"],["2","1","square dirt"],["3","1","square dirt"],["4","1","square dirt"],["5","1","square dirt"],["6","1","square dirt"],["7","1","square dirt"],
["8","1","square dirt"],["9","1","square dirt"],["10","1","square dirt"],["11","1",""],["12","1",""],["13","1",""],["14","1",""],
["15","1",""],["16","1",""],["17","1",""],["18","1",""],["19","1",""],["20","1",""],["21","1",""],["22","1",""],["23","1",""],["24","1",""],["25","1",""],["26","1",""],["27","1",""],["28","1",""],["29","1",""],["30","1",""],["31","1",""],["32","1",""],["33","1",""],["34","1",""],["35","1",""],["36","1",""],["37","1",""],["38","1",""],["39","1",""],["40","1",""],["41","1",""],["42","1",""],["43","1",""],["44","1",""],["45","1",""],["46","1",""],["47","1",""],["48","1",""],["49","1",""],["50","1",""],["51","1",""],["52","1",""],["53","1",""],["54","1",""],["55","1",""],["56","1",""],["57","1",""],["58","1",""],["59","1",""],["60","1",""],["61","1",""],["62","1",""],["63","1",""],["64","1",""],["65","1",""],["66","1",""],["67","1",""],["68","1",""],
["69","1",""],["70","1",""],["71","1",""],["72","1",""],["73","1",""],["74","1",""],["75","1",""],["76","1",""],["77","1",""].,
["78","1",""],["79","1",""],["80","1",""],["81","1",""],["82","1",""],["83","1",""],["84","1",""],["85","1",""],["86","1",""],["87","1",""],["88","1",""],["89","1",""],["90","1",""],["91","1",""],["92","1",""],["93","1",""],["94","1",""],["95","1",""],
["96","1",""],["97","1",""],["98","1",""],["99","1",""],["100","1",""]],"s":0}],

etc.

So for some reason the {x} and {y} of the for loop are send over ?

Thank you!

Oh, sorry. That’s it. You should change your traversal. Your loop should be this:

  <%= for {{x, y}, class} <- @rendered_board do %>
      <div  id={"p#{x}_#{y}"}  class={class}></div>
  <% end  %>

And you make sure to store all points in the rendered board in the initial render. If that’s too large, you can use a Stream to lazily generate a board for the initial render.

3 Likes

Yes that was it !!! Thank you so much Jose!

Each response after the player moves is now only 255 bytes containing just the squares that were changed!:

["4","121","lv:phx-Fr-yMC20e1jmpARE","phx_reply",{"response":{"diff":{"2":{"0":{"0":"{24, 67}","2":"%{1 =&gt; 
%{pos: {24, 67}, socket_id: &quot;phx-Fr-yMC20e1jmpARE&quot;}}",
"3":{"d":[["24","67","square me"],["25","67","square empty"]]}}}}},"status":"ok"}]
1 Like