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.