Liveview is not updating

I have a LiveComponent that builds a <table> from a map.
Everything worked fine until I removed an IO.inspect from the beginning of the heredoc.
Now I don’t get updates (the first time the component is rendered, but not when its changed). If I put the inspect back in everything works… :face_with_monocle:

~L"""
  <% IO.inspect m %> <---- when I take this away: no more updates
  <%= for {_k, v} <- m do %>
    <%= inspect v %>
  <% end %>
"""

Maybe change your for loop inspect to:
<%= IO.inspect v %>

What if you store m as an assign? I seem to recall something about variables not being change-tracked like assigns are.

3 Likes

that would output the inspect into the html.

I’m calling the component with an ID of the thing I want to display, this ID and the list of all those things are in the components assigns, sth like:

def render(%{display_this: id, all_the_things: things} = assigns) do
  {m, related_thing} = get_info(id, things)

Putting the thing into the assigns would take away work from the component that belongs there.

It seems like if I do anything else with the map (like <%= m.field %>) inside the table it works. If I only access the map in the loop, there are no updates. :thinking:

Is get_info a pure function? I mean, did id or things change?
It is a good discipline to make sure there is nothing but pure functions in the rendering.

yes, the function is pure, the only odd thing I’m doing is, that the maps i grab ID from are not directly in the assings but in another map (state). Here is the full code of the component (omitting the heredoc)

def render(%{selected_param: selected_param, state: state} = assigns) do
    {ref, type} = get_param_info(selected_param, state)

...

def get_param_info(
      ref_id,
      %{
        parameter_refs: parameter_refs,
        parameter_types: parameter_types
      }
    ) do
  ref = Map.get(parameter_refs, ref_id)
  type = Map.get(parameter_types, Map.get(ref, :parameterType))
  {ref, type}
end

Maybe the pin operator? ^,e.g.

~L"""
  <% IO.inspect m %> <---- when I take this away: no more updates
  <%= for {_k, v} <- ^m do %>
    <%= inspect v %>
  <% end %>
"""

thats a compile-error. pin is for matches only.

It seems like what you’re doing is explicitly warned against in the documentation: Live EEx Pitfalls

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

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

4 Likes

OK, I really should rtfm. The problem with liveview is, its too simple, everything just works (until now).

I removed the function call from the component and I’ve still the same problem.

I changed this:

# MyComponent.ex
def render(%{display_this: id, all_the_things: things} = assigns) do
  {m, related_thing} = get_info(id, things)

into

# the calling leex
<%= live_component @socket, MyComponent,
         info: MyComponent.get_info(@display_this, @things)
...

# MyComponent.ex
def render(%{info: {m, related_thing}} = assigns) do
...
~L"""
  <% IO.inspect m %> <---- when I take this away: no more updates
  <%= for {_k, v} <- m do %>
    <%= inspect v %>
  <% end %>
"""

for now I’ve added

<div style="display: none">
   <%= inspect m %>
</div>

which does not hurt right now, but I’d really like to understand whats going on.

Also if I understand liveeex-pitfalls correctly, these things should only lead to inefficent change detection, not to complete failure to render an update.

will this work?

def render(assigns) do
~L"""
  <%= for {_k, v} <- elem(@info, 0) do %>
    <%= inspect v %>
  <% end %>
"""
2 Likes

Yes, it does. As it seems its also not OK to match on the assigns and bind an assign to a variable.

Rule: In leex only access assigns using @ (?)

What works though is

# the calling leex
<%= live_component @socket, MyComponent,
         [other: :assigns] ++ MyComponent.get_info(...)
...

where get_info() returns a keyword-list. So one can easily prepare the assigns in a form that they are accessible with @ only.

I wish someone can clarify the DOs and DONTs more. Will multiple clauses of the render function work? I can imagine the following could be useful:

def render(%{live_action: :fly} = assigns) do
~L"""
...
"""
end

def render(%{live_action: :drive} = assigns) do
~L"""
...
"""
end
1 Like

works perfectly :slight_smile:
We have a “Pitfalls” section which documents gotchas to look out for.

1 Like

from the pitfalls-section:

Generally speaking, avoid accessing variables inside LiveViews, as code that access variables is always executed on every render.

this is warning about inefficient code, but in my case liveview completely fails to update when I match on a field in assigns.

Could you also please comment on this:

# the calling leex
<%= live_component @socket, MyComponent,
         [other: :assigns] ++ MyComponent.get_info(...) # get_info return a keyword list
...

is this OK? Will liveview be able to detect changes in dynamically added assigns.

this is warning about inefficient code

I think you’re taking a particular line out of context. If you read the whole section, it is very clear that it doesn’t work if you do it like this:

Due to the scope of variables, LiveView has to disable change tracking whenever variables are used in the template

source: Assigns and LiveEEx templates - LiveEEx pitfalls

What you want here is something like what @derek-zhou posted, where the enumerable in your for comprehension is referencing an assign, not a variable.

Without wanting to take anything out of context I understand

LiveView has to disable change tracking whenever variables are used in the template

Generally speaking, avoid accessing variables inside LiveViews, as code that access variables is always executed on every render.

so that performance will be bad, but nothing will break.

I recommend to change the last sentence of the pitfalls from:

Avoid defining local variables, except within for, case, and friends

to:

Never define local variables, except within for, case, and friends

And print a big scary warning if the code does this.

3 Likes

Regarding the pitfalls, it also says:

with the exception of variables introduced by Elixir basic case, for, and other block constructs

Would the with construct be included in this set? I understand the with do-end block limits the scope of the temporary variables. Would something like this be ok?

<%= with x <- calculated_variable() do %>
    ... use x here
<% end %>