phoenix_live_view v0.11.1 Phoenix.LiveComponent behaviour View Source

Components are a mechanism to compartmentalize state, markup, and events in LiveView.

Components are defined by using Phoenix.LiveComponent and are used by calling Phoenix.LiveView.Helpers.live_component/3 in a parent LiveView. Components run inside the LiveView process, but may have their own state and event handling.

The simplest component only needs to define a render function:

defmodule HeroComponent do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~L"""
    <div class="hero"><%= @content %></div>
    """
  end
end

When use Phoenix.LiveComponent is used, all functions in Phoenix.LiveView are imported. A component can be invoked as:

<%= live_component @socket, HeroComponent, content: @content %>

Components come in two shapes, stateless or stateful. The component above is a stateless component. Of course, the component above is not any different compared to a regular function. However, as we will see, components do provide their own exclusive feature set.

Stateless components life-cycle

When live_component is called, the following callbacks will be invoked in the component:

mount(socket) -> update(assigns, socket) -> render(assigns)

First mount/1 is called only with the socket. mount/1 can be used to set any initial state. Then update/2 is invoked with all of the assigns given to live_component/3. The default implementation of update/2 simply merges all assigns into the socket. Then, after the component is updated, render/1 is called with all assigns.

A stateless component is always mounted, updated, and rendered whenever the parent template changes. That's why they are stateless: no state is kept after the component.

However, any component can be made stateful by passing an :id assign.

Stateful components life-cycle

A stateful component is a component that receives an :id on live_component/3:

<%= live_component @socket, HeroComponent, id: :hero, content: @content %>

Stateful components are identified by the component module and their ID. Therefore, two different component modules with the same ID are different components. This means we can often tie the component ID to some application based ID:

<%= live_component @socket, UserComponent, id: @user.id, user: @user %>

Also note the given :id is not necessarily used as the DOM ID. If you want to set a DOM ID, it is your responsibility to set it when rendering:

defmodule UserComponent do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~L"""
    <div id="user-<%= @id %>" class="user"><%= @user.name %></div>
    """
  end
end

In stateful components, mount/1 is called only once, when the component is first rendered. Then for each rendering, the optional preload/1 and update/2 callbacks are called before render/1.

Targeting Component Events

Stateful components can also implement the handle_event/3 callback that works exactly the same as in LiveView. For a client event to reach a component, the tag must be annotated with a phx-target. If you want to send the event to yourself, you can simply use the @myself assign, which is an internal unique reference to the component instance:

<a href="#" phx-click="say_hello" phx-target="<%= @myself %>">
  Say hello!
</a>

Note @myself is not set for stateless components, as they cannot receive events.

If you want to target another component, you can also pass an ID or a class selector to any element inside the targeted component. For example, if there is a UserComponent with :id of 13, it will have the DOM ID of user-13. Using a query selector, we can send an event to it with:

<a href="#" phx-click="say_hello" phx-target="#user-13">
  Say hello!
</a>

In both cases, handle_event/3 will be called with the "say_hello" event. When handle_event/3 is called for a component, only the diff of the component is sent to the client, making them extremely efficient.

Any valid query selector for phx-target is supported, provided that the matched nodes are children of a LiveView or LiveComponent, for example to send the close event to multiple components:

<a href="#" phx-click="close" phx-target="#modal, #sidebar">
  Dismiss
</a>

Preloading and update

Every time a stateful component is rendered, both preload/1 and update/2 is called. To understand why both callbacks are necessary, imagine that you implement a component and the component needs to load some state from the database. For example:

<%= live_component @socket, UserComponent, id: user_id %>

A possible implementation would be to load the user on the update/2 callback:

def update(assigns, socket) do
  user = Repo.get! User, assigns.id
  {:ok, assign(socket, :user, user)}
end

However, the issue with said approach is that, if you are rendering multiple user components in the same page, you have a N+1 query problem. The preload/1 callback helps address this problem as it is invoked with a list of assigns for all components of the same type. For example, instead of implementing update/2 as above, one could implement:

def preload(list_of_assigns) do
  list_of_ids = Enum.map(list_of_assigns, & &1.id)

  users =
    from(u in User, where: u.id in ^list_of_ids, select: {u.id, u})
    |> Repo.all()
    |> Map.new()

  Enum.map(list_of_assigns, fn assigns ->
    Map.put(assigns, :user, users[assigns.id])
  end)
end

Now only a single query to the database will be made. In fact, the preloading algorithm is a breadth-first tree traversal, which means that even for nested components, the amount of queries are kept to a minimum.

Finally, note that preload/1 must return an updated list_of_assigns, keeping the assigns in the same order as they were given.

Managing state

Now that we have learned how to define and use components, as well as how to use preload/1 as a data loading optimization, it is important to talk about how to manage state in components.

Generally speaking, you want to avoid both the parent LiveView and the LiveComponent working on two different copies of the state. Instead, you should assume only one of them to be the source of truth. Let's discuss these approaches in detail.

Imagine that the scenario we will explore is that we have a LiveView representing a board, where each card in the board is a separate component. Each card has a form that allows to update the form title directly in the component. We will see how to organize the data flow keeping either the view or the component as the source of truth.

LiveView as the source of truth

If the LiveView is the source of truth, the LiveView will be responsible for fetching all of the cards in a board. Then it will call live_component/3 for each card, passing the card struct as argument to CardComponent:

<%= for card <- @cards do %>
  <%= live_component @socket, CardComponent, card: card, board_id: @id %>
<% end %>

Now, when the user submits a form inside the CardComponent to update the card, CardComponent.handle_event/3 will be triggered. However, if the update succeeds, you must not change the card struct inside the component. If you do so, the card struct in the component will get out of sync with the LiveView. Since the LiveView is the source of truth, we should instead tell the LiveView the card was updated.

Luckily, because the component and the view run in the same process, sending a message from the component to the parent LiveView is as simple as sending a message to self:

defmodule CardComponent do
  ...
  def handle_event("update_title", %{"title" => title}, socket) do
    send self(), {:updated_card, %{socket.assigns.card | title: title}}
    {:noreply, socket}
  end
end

The LiveView can receive this event using handle_info:

defmodule BoardView do
  ...
  def handle_info({:updated_card, card}, socket) do
    # update the list of cards in the socket
    {:noreply, updated_socket}
  end
end

As the list of cards in the parent socket was updated, the parent will be re-rendered, sending the updated card to the component. So in the end, the component does get the updated card, but always driven from the parent.

Alternatively, instead of having the component directly send a message to the parent, the component could broadcast the update using Phoenix.PubSub. Such as:

defmodule CardComponent do
  ...
  def handle_event("update_title", %{"title" => title}, socket) do
    message = {:updated_card, %{socket.assigns.card | title: title}}
    Phoenix.PubSub.broadcast(MyApp.PubSub, board_topic(socket), message)
    {:noreply, socket}
  end

  defp board_topic(socket) do
    "board:" <> socket.assigns.board_id
  end
end

As long as the parent LiveView subscribes to the "board:ID" topic, it will receive updates. The advantage of using PubSub is that we get distributed updates out of the box. Now if any user connected to the board changes a card, all other users will see the change.

LiveComponent as the source of truth

If the component is the source of truth, then the LiveView must no longer fetch all of the cards structs from the database. Instead, the view must only fetch all of the card ids and render the component only by passing the IDs:

<%= for card_id <- @card_ids do %>
  <%= live_component @socket, CardComponent, card_id: card_id, board_id: @id %>
<% end %>

Now, each CardComponent loads their own card. Of course, doing so per card would be expensive and lead to N queries, where N is the number of components, so we must use the preload/1 callback to make it efficient.

Once all card components are started, they can fully manage each card as a whole, without concerning themselves with the parent LiveView.

However, note that components do not have a handle_info/2 callback. Therefore, if you want to track distributed changes on a card, you must have the parent LiveView receive those events and redirect them to the appropriate card. For example, assuming card updates are sent to the "board:ID" topic, and that the board LiveView is subscribed to said topic, one could do:

def handle_info({:updated_card, card}, socket) do
  send_update CardComponent, id: card.id, board_id: socket.assigns.id
  {:noreply, socket}
end

With send_update, the CardComponent given by id will be invoked, triggering both preload and update callbacks, which will load the most up to date data from the database.

Live component blocks

When live_component is invoked, it is also possible to pass a do/end block:

<%= live_component @socket, GridComponent, entries: @entries do %>
  New entry: <%= @entry %>
<% end %>

The do/end will be available as an anonymous function in an assign named @inner_content. The anonymous function must be invoked passing a new set of assigns that will be merged into the user assigns. For example, the grid component above could be implemented as:

defmodule GridComponent do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~L"""
    <div class="grid">
      <%= for entry <- @entries do %>
        <div class="column">
          <%= @inner_content.(entry: entry) %>
        </div>
      <% end %>
    </div>
    """
  end
end

Where the :entry assign was injected into the do/end block.

The approach above is the preferred one when passing blocks to do/end. However, if you are outside of a .leex template and you want to invoke a component passing do/end blocks, you will have to explicitly handle the assigns by giving it a clause:

live_component @socket, GridComponent, entries: @entries do
  new_assigns -> "New entry: " <> new_assigns[:entry]
end

A template rendered inside a component can use live_link calls. The live_link is always handled by the parent LiveView, as components do not provide handle_params. live_redirect from inside a component is not currently supported. For such, you must send a message to the LiveView itself, as mentioned above, which may then redirect.

Limitations

Components must only contain HTML tags at their root. At least one HTML tag must be present. It is not possible to have components that render only text or text mixed with tags at the root.

Another limitation of components is that they must always be change tracked. For example, if you render a component inside form_for, like this:

<%= form_for @changeset, "#", fn f -> %>
  <%= live_component @socket, SomeComponent, f: f %>
<% end %>

The component ends up enclosed by the form markup, where LiveView cannot track it. In such cases, you may receive an error such as:

** (ArgumentError) cannot convert component SomeComponent to HTML.
A component must always be returned directly as part of a LiveView template

In this particular case, this can be addressed by using the form_for variant without anonymous functions:

<%= f = form_for @changeset, "#" %>
  <%= live_component @socket, SomeComponent, f: f %>
</form>

This issue can also happen with other helpers, such as content_tag:

<%= content_tag :div do %>
  <%= live_component @socket, SomeComponent, f: f %>
<% end %>

In this case, the solution is to not use content_tag and rely on LiveEEx to build the markup.

Link to this section Summary

Link to this section Callbacks

Link to this callback

handle_event(event, unsigned_params, socket)

View Source (optional)
handle_event(
  event :: binary(),
  unsigned_params :: Phoenix.LiveView.Socket.unsigned_params(),
  socket :: Phoenix.LiveView.Socket.t()
) :: {:noreply, Phoenix.LiveView.Socket.t()}
Link to this callback

preload(list_of_assigns)

View Source (optional)
preload(list_of_assigns :: [Phoenix.LiveView.Socket.assigns()]) ::
  list_of_assigns :: [Phoenix.LiveView.Socket.assigns()]
Link to this callback

update(assigns, socket)

View Source (optional)