View Source Assigns and HEEx templates
All of the data in a LiveView is stored in the socket, which is a server
side struct called Phoenix.LiveView.Socket
. Your own data is stored
under the assigns
key of said struct. The server data is never shared
with the client beyond what your template renders.
Phoenix template language is called HEEx (HTML+EEx). EEx is Embedded
Elixir, an Elixir string template engine. Those templates
are either files with the .heex
extension or they are created
directly in source files via the ~H
sigil. You can learn more about
the HEEx syntax by checking the docs for the ~H
sigil.
The Phoenix.Component.assign/2
and Phoenix.Component.assign/3
functions help store those values. Those values can be accessed
in the LiveView as socket.assigns.name
but they are accessed
inside HEEx templates as @name
.
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.
Pitfalls
There are some common pitfalls to keep in mind when using the ~H
sigil
or .heex
templates inside LiveViews.
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 block constructs such as case
,
for
, if
, and others. Therefore, you must avoid code like
this in your HEEx templates:
<% 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
for LiveViews or LiveComponents:
def render(assigns) do
sum = assigns.x + assigns.y
~H"""
<%= sum %>
"""
end
Instead explicitly precompute the assign outside of render:
assign(socket, sum: socket.assigns.x + socket.assigns.y)
Unlike LiveView's render/1
callback, a function component can
modify the assigns it receives via the assign/2
, assign/3
,
assign_new/3
, and update/3
functions. Therefore, you can assign
the computed values before declaring your template:
attr :x, :integer, required: true
attr :y, :integer, required: true
def sum_component(assigns) do
assigns = assign(assigns, sum: assigns.x + assigns.y)
~H"""
<%= @sum %>
"""
end
Generally speaking, avoid accessing variables inside HEEx
templates, 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 %>
When talking about variables, it is also worth discussing the assigns
special variable. Every time you use the ~H
sigil, you must define an
assigns
variable, which is also available on every .heex
template.
Sometimes you might want to pass all assigns from one function component to
another. For example, imagine you have a complex card
component with
header, content and footer section. You might refactor your component
into three smaller components internally:
def card(assigns) do
~H"""
<div class="card">
<.card_header {assigns} />
<.card_body {assigns} />
<.card_footer {assigns} />
</div>
"""
end
defp card_header(assigns) do
...
end
defp card_body(assigns) do
...
end
defp card_footer(assigns) do
...
end
Because of the way function components handle attributes, the above code will not perform change tracking and it will always re-render all three components on every change.
Generally, you should avoid passing all assigns and instead be explicit about which assigns the child components need:
def card(assigns) do
~H"""
<div class="card">
<.card_header title={@title} class={@title_class} />
<.card_body>
<%= render_slot(@inner_block) %>
</.card_body>
<.card_footer on_close={@on_close} />
</div>
"""
end
If you really need to pass all assigns you should instead use the regular function call syntax:
def card(assigns) do
~H"""
<div class="card">
<%= card_header(assigns) %>
<%= card_body(assigns) %>
<%= card_footer(assigns) %>
</div>
"""
end
This ensures that the change tracking information from the parent component
is passed to each child component, only re-rendering what is necessary.
However, generally speaking, it is best to avoid passing assigns
altogether
and instead let LiveView figure out the best way to track changes.
To sum up:
Avoid defining local variables inside HEEx templates, except within Elixir's constructs
Avoid passing or accessing the
assigns
variable inside HEEx templates