Assigns and HEEx templates
All of the data in a LiveView is stored in the socket as assigns.
The Phoenix.LiveView.assign/2
and Phoenix.LiveView.assign/3
functions help store those values. Those values can be accessed
in the LiveView as socket.assigns.name
but they are accessed
inside LiveView templates as @name
.
Phoenix.LiveView
's built-in templates are identified by the .heex
extension (HTML EEx) or ~H
sigil. You can learn more about them
by checking the docs for Phoenix.LiveView.Helpers.sigil_H/2
.
They are an extension of regular .eex
templates with additional
features such as:
- validation of HTML
- friendly-syntax for defining HTML components
- minimize the amount of data sent over the wire by splitting static and dynamic parts
- provide change tracking to avoid recomputing parts of the template that did not change
In this section, we are going to cover how LiveView minimizes the payload over the wire by understanding the interplay between assigns and templates.
Change tracking
When you first render a .heex
template, it will send all of the
static and dynamic parts of the template to the client. Imagine the
following template:
<h1><%= expand_title(@title) %></h1>
It has two static parts, <h1>
and </h1>
and one dynamic part
made of expand_title(@title)
. Further rendering of this template
won't resend the static parts and it will only resend the dynamic
part if it changes.
The tracking of changes is done via assigns. If the @title
assign
changes, then LiveView will execute expand_title(@title)
and send
the new content. If @title
is the same, nothing is executed and
nothing is sent.
Change tracking also works when accessing map/struct fields. Take this template:
<div id={"user_#{@user.id}"}>
<%= @user.name %>
</div>
If the @user.name
changes but @user.id
doesn't, then LiveView
will re-render only @user.name
and it will not execute or resend @user.id
at all.
The change tracking also works when rendering other templates as
long as they are also .heex
templates:
<%= render "child_template.html", assigns %>
Or when using function components:
<.show_name name={@user.name} %>
The assign tracking feature also implies that you MUST avoid performing direct operations in the template. For example, if you perform a database query in your template:
<%= for user <- Repo.all(User) do %>
<%= user.name %>
<% end %>
Then Phoenix will never re-render the section above, even if the number of users in the database changes. Instead, you need to store the users as assigns in your LiveView before it renders the template:
assign(socket, :users, Repo.all(User))
Generally speaking, data loading should never happen inside the template, regardless if you are using LiveView or not. The difference is that LiveView enforces this best practice.
HEEx pitfalls
There are two common pitfalls to keep in mind when using the ~H
sigil
or .heex
templates.
When it comes to do/end
blocks, change tracking is supported only on blocks
given to Elixir's basic constructs, such as if
, case
, for
, and similar.
If the do/end block is given to a library function or user function, such as
content_tag
, change tracking won't work. For example, imagine the following
template that renders a div
:
<%= content_tag :div, id: "user_#{@id}" do %>
<%= @name %>
<%= @description %>
<% end %>
LiveView knows nothing about content_tag
, which means the whole div
will
be sent whenever any of the assigns change. Luckily, HEEx templates provide
a nice syntax for building tags, so there is rarely a need to use content_tag
inside .heex
templates:
<div id={"user_#{@id}"}>
<%= @name %>
<%= @description %>
</div>
Another pitfall of .heex
templates is related to variables. Due to the scope
of variables, LiveView has to disable change tracking whenever variables are
used in the template, with the exception of variables introduced by Elixir
basic case
, for
, and other block constructs. Therefore, you must avoid
code like this in your LiveEEx:
<% some_var = @x + @y %>
<%= some_var %>
Instead, use a function:
<%= sum(@x, @y) %>
Similarly, do not define variables at the top of your render
function:
def render(assigns) do
sum = assigns.x + assigns.y
~H"""
<%= sum %>
"""
end
Instead explicitly precompute the assign in your LiveView, outside of render:
assign(socket, sum: socket.assigns.x + socket.assigns.y)
Generally speaking, avoid accessing variables inside LiveViews, as code that
access variables is always executed on every render. This also applies to the
assigns
variable. The exception are variables introduced by Elixir's block
constructs. For example, accessing the post
variable defined by the comprehension
below works as expected:
<%= for post <- @posts do %>
...
<% end %>
To sum up:
Avoid passing block expressions to library and custom functions, instead prefer to use the conveniences in
HEEx
templatesAvoid defining local variables, except within Elixir's constructs