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 the dynamic parts of the template, 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.

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 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. Since LiveView cannot track sum or title, if either value changes, both must be re-rendered by LiveView.

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

  ~H"""
  <h1>{title}</h1>

  {sum}
  """
end

Instead use the assign/2, assign/3, assign_new/3, and update/3 functions to compute it. Any assign defined or updated this way will be marked as changed, while other assigns like @title will still be tracked by LiveView.

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

The same functions can be used inside function components too:

attr :x, :integer, required: true
attr :y, :integer, required: true
attr :title, :string, required: true
def sum_component(assigns) do
  assigns = assign(assigns, sum: assigns.x + assigns.y)

  ~H"""
  <h1>{@title}</h1>

  {@sum}
  """
end

Generally speaking, avoid accessing variables inside HEEx templates, as code that access variables is always executed on every render. 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 %>

The assigns variable

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. However, we must avoid accessing this variable directly inside templates and instead use @ for accessing specific keys. This also applies to function components. Let's see some examples.

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. This is the only case where accessing assigns inside templates is acceptable:

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.

Summary

To sum up:

  1. Avoid defining local variables inside HEEx templates, except within Elixir's constructs

  2. Avoid passing or accessing the assigns variable inside HEEx templates