Introduction to Phoenix LiveView LiveComponents

LiveComponents are a new feature of Phoenix LiveView, bringing lightweight reusable components into the LiveView ecosystem.

[Sept. 7, 2020 - Article and code updated to LiveView v0.14.4]

Phoenix LiveView #

Phoenix LiveView became publicly available in March of 2019 and has been under active development ever since; as such we can expect regular changes and additions until the 1.0 released, likely sometime in 2020.

LiveView enables client-side dynamism using server side processes. Built on top of Elixir’s Phoenix web framework, LiveView allows you to create pages that are dynamically updated when state changes on the server, providing dynamism to web pages. And LiveView reacts to user actions, in a way that until now, only client-side programming could do.

A LiveView is an Elixir process, essentially a GenServer. When a client browser connects to a LiveView an initial rendering of the HTML is returned to the browser (good for SEO), which then in-turn establishes a persistent connection with the server. Once the connection is made, user actions are sent to the LiveView process and updates to server-side state are sent down to the browser, efficiently refreshing the page by modifying the DOM.

This allows you to create dynamic web apps without employing heavy-weight client-side frameworks or trafficking in JSON. While some targeted JavaScript will likely be required in your app, the majority of your development time is spent in Elixir, which is well-known for its developer productivity.

Elixir Phoenix apps are known for their stability and speedy response times. This is due to the lightweight processes and soft-realtime Erlang VM underlying Elixir apps. But LiveView web apps are even faster!

First, LiveViews can respond to user events in the same process, without loading a new page. The LiveView docs make the point that:

“by keeping a persistent connection between client and server, LiveView applications can react faster to user events as there is less work to be done and less data to be sent compared to stateless requests that have to authenticate, decode, load, and encode data on every request.”

Second, when transitioning between LiveView web pages, the new page can load over the same connection rather than tearing it down and establishing a new one. The LiveView docs explain that:

“if the route belongs to a different LiveView than the currently running root, then the existing root LiveView is shutdown, and an Ajax request is made to request the necessary information about the new LiveView, without performing a full static render.”

The result is super-snappy web apps that your users will love.

And now… LiveComponents bring another key facet to the LiveView story.

LiveView LiveComponents #

LiveView LiveComponents allow you to package-up markup, state, and functionality into reusable components that you can use in your LiveViews.

LiveComponents operate similarly to LiveViews, but execute in the context of a parent LiveView process, allowing you to create LiveView web pages that are composed of multiple LiveComponents, which can be reused across multiple LiveViews and apps.

And, LiveComponents can be nested inside other LiveComponents, allowing you to assemble larger aggregate components from smaller ones.

The LiveComponent documentation is very good and describes the mechanics of creating and using LiveComponents and design considerations.

In this article we will walk through the features of LiveComponents with simple and admittedly contrived, but complete, examples. In a separate article, we walk through a more useful Modal LiveComponent that you could use across multiple apps.

You can find the code for this article on GitHub.

The Most Basic LiveComponent #

Minimal LiveComponent #

A LiveComponent is an Elixir module that, at a minimum, uses the Phoenix.LiveComponent macro and defines a render/1 function.

This is a LiveComponent:

defmodule DemoWeb.StaticTitleComponent do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~L"""
    <h1>Title</h1>
    """
  end
end

Pretty simple, right? Hardly useful, but you could drop this into any markup where you happen to need a “Title” heading.

Screen Shot 2019-11-22 at 9.05.59 PM.png


LiveView using a minimal LiveComponent #

To use the StaticTitleComponent (above) in a parent LiveView, you call the live_component/3 function from within the LiveView’s render/1 function or .leex HTML template and pass the LiveView’s socket and LiveComponent module.

defmodule DemoWeb.StaticTitleLiveView do
  use Phoenix.LiveView

  def render(assigns) do
    ~L"""
    <div>
    <%= live_component(
          @socket,
          DemoWeb.StaticTitleComponent
        )
    %>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  def handle_params(_params, _uri, socket) do
    {:noreply, socket}
  end
end

Passing State into a LiveComponent #

It is rare that we would want a heading that is titled “Title”, so it would be nice if we could customize the component’s heading text. We do that by passing a keyword list of assigns in the live_component/3 call.

LiveView passing assigns to LiveComponent #

We pass the title into the LiveComponent from the LiveView like this:

defmodule DemoWeb.AssignsTitleLiveView do
  use DemoWeb, :live_view

  def render(assigns) do
    ~L"""
    <div>
    <%= live_component(
          @socket,
          DemoWeb.AssignsTitleComponent,
          title: "Assigns Title"
        )
    %>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  def handle_params(_params, _uri, socket) do
    {:noreply, socket}
  end
end
LiveComponent receiving assigns #

The LiveComponent receives the assigns passed in the live_component/3 call in its update/2 function. The component’s update/2 call will typically just take the passed in assigns and add them to its own socket assigns. The component’s socket assigns are then available for use in its render/1 function.

Note, the default implementation of update/2 merges the assigns into the LiveComponent’s socket, but let’s keep things explicit.

defmodule DemoWeb.AssignsTitleComponent do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~L"""
    <h1><%= @title %></h1>
    """
  end

  def mount(socket) do
    {:ok, socket}
  end

  def update(%{title: title} = _assigns, socket) do
    {:ok, assign(socket, title: title)}
  end
end

Stateless vs. Stateful LiveComponents #

A LiveComponent is considered either stateless or stateful.

Stateful components can be re-rendered based on changes in their own state, whereas stateless components are only re-rendered when their hosting LiveView is re-rendered.

Stateful components handle their own events, whereas events in stateless components are handled in the component’s parent.

The terminology is a little bit confusing, as a stateless component can have state that is passed in and stored in its socket assigns, but it can only be updated–and hence re-rendered–by its parent. That is, since a stateless component cannot itself respond to events, there is no way for it to be updated independent of its parent. Or you could say its state is managed by its parent.

Stateless LiveComponents #

A stateless component is updated based on state changes in its LiveView parent. That is, a stateless component is re-rendered when its parent is re-rendered.

Continuing with our Title example, let’s create a small form in the component to update the component’s heading text, initially set to “Initial Title”. Clicking the Set button, will submit the form data to the component’s parent LiveView, which updates the title in its socket assigns, causing the heading to be updated.

Screen Shot 2019-11-22 at 8.59.59 PM.png


The event generated when the form’s submit button is clicked is handled in a handle_event/3 function presiding in the stateless component’s parent LiveView. The handle_event/3 function updates internal state (socket assigns), which causes for the LiveView and LiveComponent to be re-rendered (if those assigns are referenced in the LiveView’s render function.)

Screen Shot 2019-12-01 at 9.06.48 AM.png

Stateless LiveComponent #

In the case of the stateless component, you will notice that, even though the LiveComponent renders the form, there is no corresponding handle_event/3 function for the submit button in the component itself.

defmodule DemoWeb.StatelessComponent do
  use Phoenix.LiveComponent
  use Phoenix.HTML

  def render(assigns) do
    ~L"""
    <h2><%= @title %></h2>
    <div>
    <%= f = form_for :heading, "#", [phx_submit: :set_title] %>
      <%= label f, :title %>
      <%= text_input f, :title %>
      <div>
        <%= submit "Set", phx_disable_with: "Setting..." %>
      </div>
    </form>
    """
  end

  def mount(socket) do
    {:ok, socket}
  end

  def update(%{title: title}, socket) do
    {:ok, assign(socket, title: title)}
  end
end
LiveView using stateless LiveComponent #

When the Set button on the component is clicked, the event is sent to the parent LiveView (below.) The parent LiveView’s handle_event/3 function simply updates title in its socket assigns. But since @title is referenced in the render/1 function, the LiveView, including the LiveComponent, are immediately re-rendered.

defmodule DemoWeb.StatelessComponentLiveView do
  use DemoWeb, :live_view

  def render(assigns) do
    ~L"""
    <div>
    <%= live_component(
          @socket,
          DemoWeb.StatelessComponent,
          title: @title
        )
    %>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok,
     assign(socket,
       title: "Initial Title"
     )}
  end

  def handle_params(_params, _uri, socket) do
    {:noreply, socket}
  end

  def handle_event(
        "set_title",
        %{"heading" => %{"title" => updated_title}},
        socket
      ) do
    {:noreply, assign(socket, title: updated_title)}
  end
end

Stateful LiveComponents #

A stateful component is denoted by specifying an :id in the live_component/3 assigns. Otherwise, it is considered a stateless component. The actual id is the combination of the component module name and the :id field, so you can have duplicate ids across different types of components.

    <%= live_component(
          @socket,
          DemoWeb.StatefulComponent,
          id: "1",
          title: @title
        )
    %>

Stateful components can handle events and modify their own state. Meaning that they can be re-rendered independent of their parent LiveView.

Using our example from above, when the form’s Set button is clicked handle_event/3 is invoked in the LiveComponent with the new title. The event reaches the intended component by specifying a phx-target attribute on the form with an HTML id selector defined within the component’s markup (see example below).

The handle_event/3 function updates the title in its socket assigns, which causes for the component to be re-rendered.

Screen Shot 2019-12-01 at 9.05.21 AM.png

Stateful LiveComponent #

Here is what our stateful component will look like:

defmodule DemoWeb.StatefulComponent do
  use Phoenix.LiveComponent
  use Phoenix.HTML

  def render(assigns) do
    ~L"""
    <h2><%= @title %></h2>
    <div id="stateful-<%= @id %>">
    <%= f = form_for :heading, "#", 
[phx_submit: :set_title, "phx-target": "#stateful-#{@id}"]  %>
      <%= label f, :title %>
      <%= text_input f, :title %>
      <div>
        <%= submit "Set", phx_disable_with: "Setting..." %>
      </div>
    </form>
    """
  end

  def mount(socket) do
    {:ok, socket}
  end

  def update(%{title: title, id: id}, socket) do
    {:ok,
     assign(socket,
       title: title,
       id: id
     )}
  end

  def handle_event(
        "set_title",
        %{"heading" => %{"title" => updated_title}},
        socket
      ) do
    {:noreply, assign(socket, title: updated_title)}
  end
end
LiveView using stateful LiveComponent #

And the corresponding parent LiveView:

defmodule DemoWeb.StatefulComponentLiveView do
  use DemoWeb, :live_view

  def render(assigns) do
    ~L"""
    <div>
    <%= live_component(
          @socket,
          DemoWeb.StatefulComponent,
          id: "1",
          title: @title
        )
    %>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok,
     assign(socket,
       title: "Initial Title"
     )}
  end

  def handle_params(_params, _uri, socket) do
    {:noreply, socket}
  end
end

Stateful Component Preload #

If you need to render multiple components of the same type and it would be beneficial to initialize them all at once, you can do so by defining a preload/1 function in the stateful component. This could be useful, for example, if you would like to get the components’ state from a database in a single query, rather than a separate query for each component, avoiding the N+1 query problem.

The preload/1 function is called before the components’ update/2 functions. It receives a list of the component assigns and maps those to a new set of assigns for each component, which will receive the mapped assigns in their update/2 functions. In the process, the preload/1 function could create new assigns that were not in the original live_component/3 assigns keyword list.

In our example, we will use the preload/1 function to initialize the heading title of each component.

Screen Shot 2019-11-23 at 4.57.55 PM.png


LiveComponent with preload #

In the preload/1 function we generate new titles by appending each component’s id to its given title.

defmodule DemoWeb.StatefulPreloadComponent do
  use Phoenix.LiveComponent
  use Phoenix.HTML

  def render(assigns) do
    ~L"""
    <h2><%= @title %></h2>
    <div id="preload-<%= @id %>">
    <%= f = form_for :heading, "#", 
[phx_submit: :set_title, "phx-target": "#preload-#{@id}"] %>
      <%= label f, :title %>
      <%= text_input f, :title %>
      <div>
        <%= submit "Set", phx_disable_with: "Setting..." %>
      </div>
    </form>
    """
  end

  def mount(socket) do
    {:ok, socket}
  end

  def update(%{title: title, id: id}, socket) do
    {:ok,
     assign(socket,
       title: title,
       id: id
     )}
  end

  def preload(list_of_assigns) do
    Enum.map(list_of_assigns, fn %{id: id, title: title} ->
      %{id: id, title: "#{title} #{id}"}
    end)
  end

  def handle_event(
        "set_title",
        %{"heading" => %{"title" => updated_title}},
        socket
      ) do
    {:noreply, assign(socket, title: updated_title)}
  end
end

LiveView using LiveComponents with preload #

The parent LiveView creates multiple instances of the LiveComponent, but otherwise it doesn’t need to do anything special for preload/1 to be called on the components.

Keep in mind that preload/1 will only be called once each time the components as a group are updated.

defmodule DemoWeb.StatefulPreloadComponentLiveView do
  use DemoWeb, :live_view

  def render(assigns) do
    ~L"""
    <div>
    <%= live_component(
          @socket,
          DemoWeb.StatefulPreloadComponent,
          id: "1",
          title: @title
        )
    %>
    </div>
    <div>
    <%= live_component(
          @socket,
          DemoWeb.StatefulPreloadComponent,
          id: "2",
          title: @title
        )
    %>
    </div>
    <div>
    <%= live_component(
          @socket,
          DemoWeb.StatefulPreloadComponent,
          id: "3",
          title: @title
        )
    %>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok,
     assign(socket,
       title: "Initial Title"
     )}
  end

  def handle_params(_params, _uri, socket) do
    {:noreply, socket}
  end
end

Communicating between LiveComponent and LiveView #

A LiveComponent can communicate with its parent LiveView using PubSub or by sending a message to self. Since the component runs in its parent’s process, messages sent to self are received by the parent LiveView. Components themselves cannot receive process messages, only the hosting LiveView can.

LiveComponent sending message #

A LiveComponent sends a message to its parent using send/2. The message format is arbitrary, but it is good practice to include a reference to the module with __MODULE__ to guarantee that the messages are unambiguously from the component.

Here the message is sent from the component’s handle_event/3 function.

defmodule DemoWeb.StatefulSendSelfComponent do
  use Phoenix.LiveComponent
  use Phoenix.HTML

  def render(assigns) do
    ~L"""
    <h2><%= @title %></h2>
    <div id="<%= @id %>">
    <%= f = form_for :heading, "#",
[phx_submit: :set_title, "phx-target": "##{@id}"] %>
      <%= label f, :title %>
      <%= text_input f, :title %>
      <div>
        <%= submit "Set", phx_disable_with: "Setting..." %>
      </div>
    </form>
    """
  end

  def mount(socket) do
    {:ok, socket}
  end

  def update(%{title: title, id: id}, socket) do
    {:ok,
     assign(socket,
       title: title,
       id: id
     )}
  end

  def handle_event(
        "set_title",
        %{"heading" => %{"title" => updated_title}},
        socket
      ) do
    send(
      self(),
      {__MODULE__, :updated_title, %{title: updated_title}}
    )

    {:noreply, socket}
  end
end
LiveView receiving message #

A LiveView receives messages sent by its LiveComponents in handle_info/2 functions by pattern matching on the message format.

defmodule DemoWeb.StatefulSendSelfComponentLiveView do
  use DemoWeb, :live_view

  def render(assigns) do
    ~L"""
    <div>
    <%= live_component(
          @socket,
          DemoWeb.StatefulSendSelfComponent,
          id: "stateful-send-self-component",
          title: @title
        )
    %>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok,
     assign(socket,
       title: "Initial Title"
     )}
  end

  def handle_params(_params, _uri, socket) do
    {:noreply, socket}
  end

  def handle_info(
        {DemoWeb.StatefulSendSelfComponent, 
        :updated_title, 
        %{title: updated_title}},
        socket
      ) do
    {:noreply, assign(socket, title: updated_title)}
  end
end

Live Component Blocks #

LiveComponent blocks allow you to pass an anonymous function into a component that can be used in the component’s rendering. That means you can pass in markup! The anonymous function can receive a keyword list of parameters that are available to the block at render time. The parameters to the block are arbitrary and don’t have to track with the assigns passed into the component.

We specify the block in a do/end block in the live_component/3 call. The code within the block is made available as an anonymous function named inner_content in the list of assigns passed to the component’s update/2 function. The component will typically save the anonymous function to its socket assigns so that it can be called from in its render/1 function.

Blocks LiveView #

In this example, we provide a block that styles the title heading, drawing it in orange.

defmodule DemoWeb.ComponentBlocksLiveView do
  use DemoWeb, :live_view

  def render(assigns) do
    ~L"""
    <div>
    <%= live_component @socket,
                       DemoWeb.ComponentBlocksComponent,
                       title: "Title" do %>
      <h1 style="color: orange;">
        <%= @title_passed_from_component %>
      </h1>
    <% end %>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  def handle_params(_params, _uri, socket) do
    {:noreply, socket}
  end
end

Blocks LiveComponent #

In the LiveComponent, the inner_content anonymous function is copied to the socket assigns in the update/2 function and then called in the render/1 function, passing the parameters that will be available to the block.

defmodule DemoWeb.ComponentBlocksComponent do
  use Phoenix.LiveComponent
  use Phoenix.HTML

  def render(assigns) do
    ~L"""
    <%= @render_title.(title_passed_from_component: @title) %>
    """
  end

  def mount(socket) do
    {:ok, socket}
  end

  def update(%{title: title, inner_content: inner_content}, socket) do
    {:ok,
     assign(socket,
       title: title,
       render_title: inner_content
     )}
  end
end

Screen Shot 2019-11-23 at 5.59.48 PM.png


Design Considerations #

There should be a single source of truth between a LiveView and embedded LiveComponent. State shouldn’t straddle the two. It should either be in the LiveView or the LiveComponent, not both. The LiveComponent documentation has a detailed discussion describing the two options.

Wrapping Up #

LiveComponents allow us to better structure our LiveView code and reuse components across multiple LiveViews and apps. Perhaps someday we will see a robust ecosystem of open-source and commercial LiveComponents to speed the development of robust, fast, and performant LiveView web apps.

If you would like to explore LiveComponents further, see our follow-up article: Creating a Modal LiveView LiveComponent.

Thanks to Bruce Tate, Moxley Stratton, and Ben Munat for their feedback.

Links #

Creating a Modal LiveView LiveComponent: http://blog.pthompson.org/phoenix-liveview-livecomponent-modal

LiveComponent Examples Repo: https://github.com/pthompson/live_component_examples

LiveComponent Documentation: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html

LiveView Documentation: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html

LiveView GitHub Repo: https://github.com/phoenixframework/phoenix_live_view

 
245
Kudos
 
245
Kudos

Now read this

Creating a Modal LiveView LiveComponent

In this article, we will create a reusable and configurable modal dialog that we can use throughout our app. We will implement the modal as a LiveComponent and along the way we will touch on several other important LiveView features... Continue →