DEV Community

Cover image for LiveView Assigns: Three Common Pitfalls and Their Solutions
Karol Słuszniak for AppSignal

Posted on • Originally published at blog.appsignal.com

LiveView Assigns: Three Common Pitfalls and Their Solutions

In the first part of this two-part series, we examined LiveView assigns in detail — demystifying assigns, looking at some key concepts, and debugging.

Now, we'll turn our attention to three common mistakes that you might make with assigns and how to avoid them.

Let's get started!

1. Evaluating All LiveView Assigns

As you pass assigns around to view helpers, and the complexity increases, you may need many assigns in some functions. For example:

<%= user_note(@user, @note, @theme, @locale) %>
Enter fullscreen mode Exit fullscreen mode

Then you may be tempted to do the following instead:

<%= user_note(assigns) %>
Enter fullscreen mode Exit fullscreen mode

The problem with this simplification is that it’ll completely ruin change tracking and, as a result, changing any assign will trigger an update.

To solve this, stick to passing only the required assigns explicitly and collapse multiple arguments into a keyword list if needed:

<%= user_note(@user, @note, theme: @theme, locale: @locale) %>
Enter fullscreen mode Exit fullscreen mode

Note: LiveView itself hints at, and counters this problem by excluding all other assigns from the widely used @socket struct in which they’re generally stored. But it does not forbid you from reaching for the assigns directly, opening a door to this issue.

2. Re-rendering Entire Lists

Change tracking on nested data such as lists is a complex problem, regardless of the framework. LiveView goes the extra mile to represent for loops via a dedicated struct so that static parts are only sent once. But when it comes to assigns, it tracks all that appear in such loops as a whole.

Our generated live resource in the 'Caveman Debugging in LiveView' section of the first part of this series re-evaluated every table row and cell regardless of what we did with the @notes assign.

There’s a solution though. We can establish a separate tracking context using stateful live components. So let’s try it out and create a component for each note:

defmodule MyAppWeb.NotesLive.Index.NoteRow do
  use MyAppWeb, :live_component
  def render(assigns) do
    ~H"""
    <tr id={"note-#{@note.id}"}>
      <td><%= inspect(Time.utc_now()) %></td>
      <td><%= inspect({Time.utc_now(), @note.name}) %></td>
      <td><%= inspect({Time.utc_now(), @note.content}) %></td>
      <!-- ACTIONS (CUT) -->
    </tr>
    """
  end
end
Enter fullscreen mode Exit fullscreen mode

Then render it within the table:

<table>
  <!-- TABLE HEADER (CUT) -->
  <tbody id="notes">
    <%= for note <- @notes do %> <.live_component module={__MODULE__.NoteRow}
    id={"note-row-#{note.id}"} note={note} /> <% end %>
  </tbody>
</table>
Enter fullscreen mode Exit fullscreen mode

Now you may once again create, edit, and delete some notes for the following highly desired behavior:

  • When creating, only the newly added row gets updated.
  • When editing, only the cells for changed fields get updated.
  • When deleting, no other rows get updated.

Note: You should not have to worry about memory usage caused by copying assigns to live components. They reside on the same process as the parent view, and so should share immutable data.

The excellent article Optimising data-over-the-wire in Phoenix LiveView compared Websocket payload for the naive loop vs. the component-based version. Note, however, that it may not reflect the current state of affairs as these things are rapidly evolving — see the Phoenix LiveView changelog.

3. Growing LiveView Assigns Infinitely

As noted in the 'LiveView Assigns Manage State' part of the previous post, the server-side nature of LiveView places extra responsibilities on memory management. For that reason, you can't afford to grow lists infinitely, e.g., when paginating, as shown below:

defmodule MyAppWeb.NotesLive do
  def render(assigns) do
    ~H"""
    <div id="notes">
      <%= for note <- @notes do %>
        <div id={"note-#{note.id}"}>
          <!-- CUT (note) -->
        </div>
      <% end %>
    </div>
    <button phx-click="load_more">Load more</button>
    """
  end
  def handle_event("load_more", _, socket) do
    next_page = socket.assigns.last_page + 1
    more_notes = Notes.list_notes(page: next_page)
    {:noreply, assign(socket,
      notes: socket.assigns.notes ++ more_notes,
      last_page: next_page
    )}
  end
end
Enter fullscreen mode Exit fullscreen mode

Here, the memory usage will grow linearly, not just with the number of users (which is a problem we definitely don't want to have) but also with the total number of notes.

The solution is to mark that assign as temporary, switch to the append update model for the note listing, and only assign new items:

defmodule MyAppWeb.NotesLive do
  def mount(params, session, socket) do
    # ...
    {:ok, socket, temporary_assigns: [notes: []]}
  end
  def render(assigns) do
    ~H"""
    <div id="notes" phx-update="append">
      <!-- CUT (notes loop) -->
    </div>
    """
  end
  def handle_event("load_more", _, socket) do
    next_page = socket.assigns.last_page + 1
    more_notes = Notes.list_notes(page: next_page)
    {:noreply, assign(socket,
      notes: more_notes,
      last_page: next_page
    )}
  end
end
Enter fullscreen mode Exit fullscreen mode

Now the notes list will still accumulate newly loaded records while keeping memory usage under control.

Wrap Up

In the first part of this series, we spent some time diving into LiveView assigns. First, we demystified assigns, before embarking on some caveman debugging and socket inspection.

In this article, we examined three common pitfalls — evaluating all assigns, re-rendering entire lists, and growing assigns infinitely — and their solutions.

I hope that this series has given you an idea of some trade-offs that come with LiveView assigns, alongside ways to get the most out of the wonderful technology that Phoenix LiveView definitely is.

Happy assigning!

A note from AppSignal: We're very excited to have recently released AppSignal for Phoenix 2.1, which adds automatic instrumentation for LiveView through Telemetry.

P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!

Top comments (1)

Collapse
 
katafrakt profile image
Paweł Świątkowski

This is very useful, especially point 2. I'm usually using functional components for rows, so I'm accidentally avoiding the problem, but it's good to be aware of it in the first place ;)