Phoenix LiveView list comprehension and conditional evaluation

I’m trying to get my head around Phoenix.LiveView.

I have a Git diff table where I want to display reviews, a review can be created on each diff line.
A review is basically a comment thread. Basically a feature copy of GitHub’s commit reviews…

My .leex template looks as follow:

<%= for delta <- @deltas do %>
  <div class="columns">
    <table class="blob-table diff-table" >
      <tbody>
        <%= for {hunk, hunk_index} <- Enum.with_index(delta.hunks) do %>
          <tr class="hunk">
            <td class="line-no" colspan="2"></td>
            <td class="code" colspan="2">
              <div class="code-inner nohighlight"><%= hunk.header %></div>
            </td>
          </tr>
          <%= for {line, line_index} <- Enum.with_index(hunk.lines) do %>
            <%= cond do %>
              <% line.origin == "+" -> %>
                <tr class="diff-addition">
              <% line.origin == "-" -> %>
                <tr class="diff-deletion">
              <% true -> %>
                <tr>
            <% end %>
              <td class="line-no"><%= if line.old_line_no != -1, do: line.old_line_no %></td>
              <td class="line-no"><%= if line.new_line_no != -1, do: line.new_line_no %></td>
              <td class="code origin">
                <button class="button is-link is-small" phx-click="new-line-comment" phx-value="<%= oid_fmt(delta.new_file.oid) %>:<%= hunk_index %>:<%= line_index %>">
                  <span class="icon"><i class="fa fa-comment-alt"></i></span>
                </button>
                <%= line.origin %>
              </td>
              <% highlight_lang = highlight_language_from_path(delta.new_file.path) %>
              <td class="code">
                <div class="code-inner hljs <%= highlight_lang %>"><%= line.content %></div>
              </td>
            </tr>
            <%= cond do %>
              <% line_review = Enum.find(@line_reviews, &(&1.blob_oid == delta.new_file.oid && &1.hunk == hunk_index && &1.line == line_index)) -> %>
                <tr class="inline-comments">
                  <td colspan="4">
                    <%= live_render(@socket, GitGud.Web.CommentThreadView, session: %{line_review_id: line_review.id}, child_id: "CommitLineReview-#{line_review.id}") %>
                    <div class="comment-form">
                      <%= if @line_review_form == {line_review.blob_oid, line_review.hunk, line_review.line} do %>
                        <%= form_for GitGud.Comment.changeset(%GitGud.Comment{}), "#", [phx_change: "validate-comment", phx_submit: "create-line-comment"], fn f -> %>
                          <div class="field">
                            <div class="control">
                              <%= textarea f, :body, class: "textarea" %>
                            </div>
                          </div>
                          <div class="field is-grouped">
                            <div class="control">
                              <button class="button" type="reset" phx-click="cancel-line-comment">Cancel</button>
                            </div>
                            <div class="control">
                              <button class="button is-success" type="submit">Add comment</button>
                            </div>
                          </div>
                        <% end %>
                      <% else %>
                        <div class="field">
                          <div class="control">
                            <input class="input" placeholder="Leave a comment" phx-focus="new-line-comment" phx-value="<%= Enum.join([oid_fmt(delta.new_file.oid), hunk_index, line_index], ":") %>" />
                          </div>
                        </div>
                      <% end %>
                    </div>
                  </td>
                </tr>
              <% @line_review_form == {delta.new_file.oid, hunk_index, line_index} -> %>
                <tr class="inline-comments">
                  <td colspan="4">
                    <div class="comment-form">
                      <%= form_for GitGud.Comment.changeset(%GitGud.Comment{}), "#", [phx_change: "validate-comment", phx_submit: "create-line-comment"], fn f -> %>
                            <div class="field">
                              <div class="control">
                                <%= textarea f, :body, class: "textarea" %>
                              </div>
                            </div>
                            <div class="field is-grouped">
                              <div class="control">
                                <button class="button" type="reset" phx-click="cancel-line-comment">Cancel</button>
                              </div>
                              <div class="control">
                                <button class="button is-success" type="submit">Add comment</button>
                              </div>
                            </div>
                        </div>
                      <% end %>
                    </div>
                  </td>
                </tr>
              <% true -> %>
            <% end %>
          <% end %>
        <% end %>
      </tbody>
    </table>
  </div>
<% end %>

My LiveView socket has @deltas and @line_reviews assigned. On each diff line, I need to check if a review is available:

line_review = Enum.find(@line_reviews, &(&1.blob_oid == delta.new_file.oid && &1.hunk == hunk_index && &1.line == line_index))

This condition is evaluated in a cond block within the template. If line_review is not nil, I use live_render/4 to render the comment-thread:

<%= live_render(@socket, GitGud.Web.CommentThreadView, session: %{line_review_id: line_review.id}, child_id: "CommitLineReview-#{line_review.id}") %>

The problem I’m facing is that each time the template is re-rendered, LiveView remount a new CommentThreadView for each associated line-review. This happens each time the user interacts with my view even when it does not have any impact on @delta (which basically never changes) or @line_reviews assigns.

Is my approach wrong? Is it even possible to combine comprehensions, conditional evaluations and nested live views in a performant way? Are they any tools/tricks I can use to debug what LiveView is evaluating (static and dynamic parts, etc.)?

We don’t perform dirty tracking inside comprehensions (it is a complex problem to check how much changed between two arbitrary lists). So if any assign inside the comprehension changes, then the whole comprehension re-renders.

One option for you to figure this out is to remove most the code and if the child live view re-renders. Then slowly add it back to see what is forcing the full re-render.

I would try to have the comment threads as values in a map. The keys would be what you need to match the comment thread to the line.

You end up trading a single preprocessing step for a simpler and faster access behavior, and LV might be able to track it better.

Ok, thank you for the clarification, that’s what I was thinking. Maybe this is something that could be explained a little bit further in the Phoenix.LiveView.Engine docs.

At the bottom, the documentation states:

The list of dynamics is always a list of iodatas, as we only perform change tracking at the root and never inside case , cond , comprehensions , etc. Similarly, comprehensions do not have fingerprints because they are only optimized at the root, so conditional evaluation, as the one seen in rendering, is not possible.

but I don’t really get what the root is referring to. The first level of static/dynamic content within a comprehension? Does it mean that if i nest a comprehension within an other there is no tracking of changes in the nested comprehension?

Yes, that’s what I tried in my first attempt, basically attaching my line-reviews at line level within my @deltas. But the LiveView engine does not seem to track changes that deep either.

1 Like

Root is the template itself. What we don’t do is tracking within each comprehension item. If any assign used anywhere within a comprehension changes, then the whole comprehension is re-rendered. But nothing in a comprehension changes, then nothing is re-rendered. Please submit a PR to clarify the docs and copy me. Thanks!

2 Likes