LiveView: Modifying assigns in render/1

I have a list view where various actions modify an initial list to be displayed. Initially I was duplicating all the logic for this filtering in every action, which worked fine but resulted in a lot of duplicated code like this:

def handle("some_action", _, socket) do
  some_assigns = do_some_action
  |> apply_some_filter
  |> another_filter
  
  socket = socket
  |> assign(:some_assigns, some_assigns)

  {:no_reply, socket}
end

So I thought I would refactor like so:

def render(assigns) do
  filtered_assigns = assigns
  |> apply_some_filter
  |> another_filter

  MyView.render("list", assigns |> Map.put(:filtered_assigns, filtered_assigns))
end

def handle("some_action", _, socket) do
  some_assigns = do_some_action
  
  socket = socket
  |> assign(:some_assigns, some_assigns)

  {:no_reply, socket}
end

This appeared to work at first, insofar as @filtered_assigns was available in the template and filtered corrected, but when I tried calling some_action it no longer updated the template, even though I verified the value of the assign was updated in the render function itself. So, e.g., Enum.count(@filtered_assigns) returned 2, but the template still included 3 items.

So, instead I moved the filters to a ‘helper’ method in MyView instead, and everything worked perfectly with no change in the filter logic itself.

I’m fine with storing that kind of logic in the view rather than modifying assigns in render, but I didn’t see anything in the docs that suggested I shouldn’t be able to do the latter. What I am I missing? I assume the assign function does something internally that Map.put doesn’t?

1 Like

The doc is here:

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#module-liveeex-pitfalls

Similarly, do not define variables at the top of your render function:

def render(assigns) do
  sum = assigns.x + assigns.y

  ~L"""
  <%= sum %>
  """
end

Instead explicitly precompute the assign in your LiveView, outside of render:

assign(socket, sum: socket.assigns.x + socket.assigns.y)

In your case LiveView knows that some_assign changed, but the template reads filtered_assigns, not some_assign, so it thinks there’s no need to re-render. As you guessed the assign function also marks the field “changed” - it’s not just a Map.put. That’s why setting filtered_assigns directly doesn’t mark that field as changed.

One pattern I’ve seen is to define a helper e.g. apply_filter that does the computation and assigns the result (filtered_assigns) to the socket, and call it every time you update a field. It works well if you need the value in multiple places in the template so you don’t recompute it every time.

2 Likes

Thanks for the detailed explanation.

I guess the thing that threw me off is that the assign being filtered did often change as a result of the actions in question. But I didn’t think about it in terms of what assigns are referenced by the template.

It does seem a bit weird to define a function that essentially only delegates to another function, and breaks if you do anything else with the variable it explicitly exposes.