Build Interactive Phoenix LiveView UIs with Components
Tags: elixir
Published: 2022-01-18 14:12:08

LiveView empowers developers to build interactive, single-page web apps with ease by providing a framework that eliminates the need for guesswork.

In this post, we'll take a look at how you can layer simple, single-purpose functional components to wrap up shared presentation logic. We'll also use more sophisticated live components to craft easy-to-maintain single-page flows that handle complex user interactions.

Along the way, you'll gain a solid understanding of working with HEEx — Phoenix and LiveView's new templating engine — and you'll see some of LiveView's out-of-the-box function components in action.

Let's dive in!

## The Feature: Compose a User Survey UI for a Phoenix LiveView App

Before we dive into writing any actual code, let's talk about the feature we'll build. Imagine that you're responsible for a Phoenix web app, Arcade, that provides in-browser games to users. A user can log in, select a game to play, and even invite friends to play games with them.

In this post, we'll build out a "user survey" feature that asks the user to fill out some demographic info about themselves and then provide a rating for each of several games. We'll focus on that second part of the survey — the game rating forms.

Let's lay out what we'll build in a bit more detail. We'll begin with a parent live view that lives at the `/survey` route, `ArcadeWeb.SurveyLive`. This live view will render a child functional component, "ratings index". The ratings index component will iterate over the games and show a game rating if one by the current user exists, or a form for a new rating if not. If users haven't completed a game rating, they will see a list of forms to rate each game 1-5 stars. If they have completed some (or all) ratings, they will see those displayed and forms to submit ratings for the games they have not yet rated.

Here's a look at how it will work:

![ratings form partially complete](https://blog.appsignal.com/images/blog/2022-01/rating-form-partially-complete.png)

With our plan in place, we're ready to start writing code.

## Define the Parent Live View

We'll build a route first, then mount and render the initial live view.

### Define the Survey Route

Our first job is to establish a route. The survey will live at `/survey`, and it should only work for authenticated users so we can deliver a survey to single, identifiable users. We'll tie the route to the yet-to-be-written `SurveyLive` live view, with the `:index` live action, like this:

```elixir
# router.ex
scope "/", ArcadeWeb do
  pipe_through [:browser, :require_authenticated_user]
    live "/survey", SurveyLive, :index
end
```

This code assumes you've used the [Phoenix Auth generator](https://github.com/aaronrenner/phx_gen_auth) to add authentication to your Phoenix app. The details of authentication aren't important for our purposes here. Just know that the `survey` route is a protected route that requires an authenticated user. This means that when a logged-in user points their browser at `/survey`, the `SurveyLive` view will mount with a `session` argument that contains a key of `"user_token"` pointing to a token we can use to identify the current user. The generator also gives us a function, `Accounts.get_user_by_session_token(user_token)`, that we will use to fetch the user for that token.

With our route established, it's time to define the `SurveyLive` live view.

### Mount the Survey Live View

The `mount/3` function builds the initial state for `SurveyLive`. Let's think a bit about that initial state. We need to use the current user to build our survey's demographic and rating portions since a demographic belongs to a user and a rating belongs to a game and a user. So we want to store that user in the live view's state. This way, we can make it available to the rating form component later. Now, let's implement a `mount/3` function that adds the current user to socket assigns, like this:

```elixir
# lib/arcade_web/live/survey_live.ex
defmodule ArcadeWeb.SurveyLive do
  use ArcadeWeb, :live_view

  def mount(_params, %{"user_token" => user_token}, socket) do
    {:ok,
      socket
      |> assign(
          :current_user,
          Accounts.get_user_by_session_token(user_token))}
  end
end
```

Okay, we're ready to render a simple version of our survey live view.

### Render the Survey Live View

We won't provide a `render/1` function, instead we'll use a template — `lib/arcade_web/live/survey_live.html.heex`. Let's keep it simple for now:

```eex
<section class="row">
  <h2>Survey</h2>
</section>
```

Reload your browser, and you'll see the bare-bones template shown here:

![simple survey template](https://blog.appsignal.com/images/blog/2022-01/survey-template-simple-content.png)

We have the basic framework for our survey UI in place. Now, we're ready to build our first function component.

## List Ratings in Phoenix LiveView

Before we build our ratings index function component, let's talk about what function components are and how they work. A function component takes in an `assigns` argument and returns a HEEx template. Function components are implemented in modules that use the `Phoenix.Component` behaviour, which also gives us a convenient syntax for rendering function components.

We're almost ready to define our function component. Let's take a step back and discuss HEEx.

### Understanding HEEx Templates

A HEEx template is any file ending in the `.heex` extension that is implicitly rendered by a live view, or any markup rendered by a live view or component that is encapsulated in the `~H"""`, `"""` tags. The HEEx templating engine is an extension of EEx. Just like EEx templates, HEEx will process template replacements within your HTML code. Everything between the `<%=` and `%>` expressions is a template replacement. HEEx will evaluate the Elixir code within those tags and replace them with the result.

HEEx does more than just templating, though. It also:

- provides compile-time HTML validations
- gives us a convenient component rendering syntax
- optimizes the amount of content sent over the wire, allowing LiveView to render _only those portions of the template that need updating when state changes_

HEEx is the default templating engine for Phoenix and LiveView. Any generated template files in your Phoenix app will be HEEx templates and end in the `.html.heex` extension. When using inline `render/1` functions in your live views, or function components, you'll return HEEx templates with the `~H` sigil.

With that basic understanding in place, we're ready to implement the ratings index function component.

### Define the Function Component

We'll build a rating index component responsible for orchestrating the state of _all_ the game ratings in our survey. This component will iterate over the games and render the rating details if a user rating exists, or the rating form if it doesn't. The responsibility for rendering rating details will be handled by a "rating show" function component. A live "rating form" component will handle rendering and managing a rating form. More on live components in a bit.

Meanwhile, `SurveyLive` will continue to be responsible for managing the overall state and appearance of the survey page. The rating index component will receive the list of game ratings to render from the parent live view. The parent live view is responsible for maintaining and updating that list.

In this way, we keep our code organized and easy to maintain because it adheres to the single responsibility principle — each component has one job to do. By layering these components within the parent `SurveyLive` view, we compose a series of small, manageable pieces into one interactive feature — the user survey page.

We'll begin by implementing the `RatingLive.Index` function component. Then, we'll move on to the rating show component, followed by the rating form component.

Create a file, `lib/arcade_web/live/rating_live/index.ex`, and key in the following component definition:

```elixir
defmodule ArcadeWeb.RatingLive.Index do
  use Phoenix.Component
  use Phoenix.HTML
  alias ArcadeWeb.RatingLive
end
```

Our function component uses the `Phoenix.Component` behaviour which we need to render HEEx templates. Any module that implements _only_ function components will use this behaviour. You'll use a different behaviour when building live or stateful components, which we'll do later on in this post.

We're also using the `Phoenix.HTML` behaviour here to bring in the `Phoenix.HTML.raw/1` function that we'll use to render unicode characters — more on that in a bit. Finally, we're aliasing the name of the component module itself so it's easy to ergonomically invoke other function components defined within the module from within our main function component here.

The entry point of our module will be the  `games/1` function. We'll call on this function component from the parent live view to render the list of games. The function will take in an `assigns` argument containing the list of games passed in from the parent `SurveyLive` view. It will return a HEEx template that iterates over that list and renders _another_ function component to show the game rating details if a rating exists and the rating form live component if not. Define that function now, as shown here:

```elixir
def games(assigns) do
  ~H"""
  <div class="survey-component-container">
    <.heading games={@games} />
    <.list games={@games} current_user={@current_user}/>
  </div>
  """
end
```

We're composing our `games/1` function out of two additional function components — `heading/1` and `list/1`. We call on those function components with the `.function_name assigns... />` syntax. This invokes the function component and passes in whatever assigns we provide as the `assigns` argument to that function.

It's also worth noting the `{}` interpolation syntax here, instead of the `<%= %>` EEx tags you might be used to. This is because HEEx, unlike EEx, isn't just responsible for evaluating and templating Elixir expressions into your HTML. It also parses and validates the HTML itself. So you can't use the traditional EEx tags _inside_ HTML tags in a HEEx template. Instead, use curly braces to interpolate values inside HTML tags and function component calls, and use EEx tags when interpolating values in the body, or inner content, of those tags.

Let's build those additional function components now, starting with `heading/1`:

```elixir
def heading(assigns) do
  ~H"""
    <h2>
      Ratings
      <%= if ratings_complete?(@games), do: raw "&#x2713;" %>
    </h2>
  """
end
```

The `heading/1` function is pretty small and single-purpose. It renders an `<h2>` element that encapsulates some text along with a helper function that checks to see if _all_ of the games have a rating by the current user. If so, we render the unicode to a checkmark and the user can see that all of the ratings forms have been completed.

Before we implement this helper function, let's see how we're going to render the index component with a list of games.

When we render this index component from the `SurveyLive` template, we'll use the `SurveyLive` view to query for the list of games with ratings by the current preloaded user.

Then, we'll pass that list of games down into the index component. So we can assume that each game in the `@games` list has its `ratings` list populated _only_ with a rating from the current user. With that in mind, we can implement the `ratings_complete?/1` function to iterate over the list of games and return `true` if there is a rating for every game. Add in your function now, like this:

```elixir
 defp ratings_complete?(games) do
  Enum.all?(games, fn game ->
    length(game.ratings) == 1
  end)
end
```

Now if a user has completed all of the game ratings, they'll see the "Ratings" header with a nice checkmark next to it:

![ratings complete header](https://blog.appsignal.com/images/blog/2022-01/ratings-complete-header.png)

With the `heading/1` function component out of the way, let's turn our attention to `list/1`. Add in this function now:

```elixir
def list(assigns) do
  ~H"""
  <%= for {game, index} <- Enum.with_index(@games) do %>
    <%= if rating = List.first(game.ratings) do %>
      <h3>Show rating coming soon!</h3>
    <% else %>
      <h3>Rating form coming soon!</h3>
    <% end %>
  <% end %>
  """
end
```

Here, we use a `for` comprehension that maps over all of the games in the system, where each game's `ratings` list contains the single preloaded rating by the given user if one exists. Inside that comprehension, the template will render the rating details if a rating exists or a form for that rating if not. Nesting components in this manner lets the reader of the code deal with a tiny bit of complexity at a time.

We'll dig into this logic a bit more when we're ready to implement these final two components. With the index component out of the way, we are ready to weave it into our `SurveyLive` template.

### Render the Component

The next bit of code we'll write shows how the presentation of our view can change based on the contents of the socket. The `SurveyLive` view will use the state of the overall survey to control what is shown to the user. This view holds the list of games, and their ratings by the current user, in state. It will pass this list into the `RatingLive.games/1` function component as part of the component assigns. The contents of this list will allow the `games/1` function component to determine if it should show rating details or a rating form.

Let's update `SurveyLive` now to query for the list of games and their ratings from the current user and add them to socket assigns, like this:

```elixir
defmodule ArcadeWeb.SurveyLive do
  use ArcadeWeb, :live_view
  alias Arcade.Survey

  def mount(_params, %{"user_token" => token}, socket) do
    {:ok,
     socket
     |> assign(:current_user, Accounts.get_user_by_session_token(token))
     |> assign(:games, Catalog.list_games_with_user_rating(user))}
  end
end
```

Assume that the `Catalog.list_games_with_user_rating/1` context function returns the list of all games, with _only the rating by the given user_ preloaded, if any.

Now we're ready to render our rating index function component from the `SurveyLive` template:

```eex
<!-- lib/arcade_web/live/survey_live.html.heex -->
  <RatingLive.Index.games games={@games}
      current_user={@current_user} />
```

Here, we're once again using the `<.function_component assigns... />` syntax to render our function component and the `{}` interpolation syntax for interpolating within tags in a HEEx template.

Now that we're rendering our `RatingLive.Index.games/1` function component with the game list, let's build the stateless function component to show the existing rating for a game.

## Show a Rating

We're getting closer to the goal of showing ratings, step by step. Remember, we'll show the existing ratings, and forms for ratings, otherwise.

Let's cover the case for ratings that exist first. We'll define a stateless component to show a rating. Then, we'll render that component from within the HEEx template returned by `RatingLive.Index.games/1`. Let's get started.

### Build the Function Component

Create a file, `lib/arcade_web/live/rating_live/show_component.ex`, and key this in:

```elixir
defmodule ArcadeWeb.RatingLive.Show do
  use Phoenix.Component
  use Phoenix.HTML
end
```

We're defining a module that uses the `Phoenix.Component` behaviour and the `Phoenix.HTML` behaviour, since we'll once again need support for the `Phoenix.HTML.raw/1` function to render unicode characters.

Okay, let's move on to the entry point of our function component, the `stars/1` function. We'll call this function from within the HEEx template returned by `RatingLive.Index.games/1` with an assigns that includes the given game's rating by the current user.

The `stars/1` function will operate on this rating and use some helper functions to construct a list of filled and unfilled unicode star characters. We'll construct that list using a simple pipeline, and then render it in a HEEx template, like this:

```elixir
def stars(assigns) do
  stars =
    filled_stars(assigns.rating.stars)
    |> Enum.concat(unfilled_stars(assigns.rating.stars))
    |> Enum.join(" ")

  ~H"""
  <div>
    <h4>
      <%= @game.name %>:<br/>
      <%= raw stars %>
    </h4>
  </div>
  """
end
```

The `filled_stars/1` and `unfilled_stars/1` helper functions are interesting. Take a look at them here:

```elixir
def filled_stars(stars) do
  List.duplicate("&#x2605;", stars)
end

def unfilled_stars(stars) do
  List.duplicate("&#x2606;", 5 - stars)
end
```

Examining our pipeline in the `stars/1` function, we can see that we call on `filled_stars/1` to produce a list of filled-in, or "checked", star unicode characters corresponding to the number of stars the game rating has. Then, we pipe that into a call to `Enum.concat/2` with a second argument of the output from `unfilled_stars/1`. This second helper function produces a list of empty, or not checked, star characters for the remaining number of stars.

For example, if the number of stars in the rating is 3, our pipeline of helper functions will create a list of three checked stars and two un-checked stars. Our pipeline concatenates the two lists together and joins them into a string of HTML that we can render in the template.

We have everything we need to display a completed rating, so it's time to roll several components up together.

### Render the Component

We're ready to implement the next phase of our plan. The `RatingLive.Index.games/1` function component iterates over the list of games in the `@games` assigns. If a rating is present, we show it. Add in the call to our new `Show.stars/1` component now, like this:

```elixir
def list(assigns) do
  ~H"""
  <%= for {game, index} <- Enum.with_index(@games) do %>
    <%= if rating = List.first(game.ratings) do %>
      <Show.stars rating={rating} game={game} />
    <% else %>
      <h3>Rating form coming soon!</h3>
    <% end %>
  <% end %>
  """
end
```

It's a straight `for` comprehension with an `if` statement. If a rating exists, we render the function component by calling on it with the `game` and `rating` assigns. If not, we need to render the form. Let's build that form and render it now.

## Submit a Rating

Our rating form will display the form and manage its state, validating and saving the rating. We'll need to pass a game and user for our database relationships, and the game's index in the parent LiveView's `socket.assigns.games` list. We'll use this index later on to update `SurveyLive` state efficiently.

### Build the Rating Form Component

The component will be stateful since it needs to manage the state of the rating form and respond to user interactions to validate form changes and handle the form submission.

A stateful, or live, component is any module that uses the `:live_component` behaviour and renders a HEEx template. Such modules can implement the live component lifecycle functions, including `mount/3`, `update/2`, and `render/1`, and respond to user events by implementing a `handle_event/3` function. Let's define our live component module now. Create a file, `lib/arcade_web/live/rating_live/form.ex`, and key this in:

```elixir
defmodule ArcadeWeb.RatingLive.FormComponent do
  use ArcadeWeb, :live_component
  alias Arcade.Survey
  alias Arcade.Survey.Rating
end
```

This is simple enough, to begin with. We define our module, use the `:live_component` behaviour, and add in some aliases that we'll need later.

We'll use LiveView's `.form/1` function (more on that in a bit) to construct the rating form. This function requires a changeset, so we'll need to store one in our component's state. Here's where the component lifecycle comes into play. When we render a live component, LiveView starts the component in the parent view's process and calls these callbacks, in order:

- `mount/1`
: The single argument is the socket, and we use this callback to set the initial state. This callback is invoked only once, when the component is first rendered from the parent live view.

- `update/2`
: The two arguments are the assigns argument given to `live_component/3` and the socket. By default, it merges the assigns argument into the `socket.assigns` established in `mount/1`. We'll use this callback to add additional content to the socket *each time `live_component/3` is called*.

- `render/1`
: The one argument is `socket.assigns`. It works like a render in any other live view.

Stateful components will always follow this process when first mounted and rendered. Then, when the component updates in response to changes in the parent live view, only the `update/2` and `render/1` callbacks fire. Since these updates skip the `mount/1` callback, the `update/2` function is the safest place to establish the component's initial state. Let's create our component's `update/2` function now, like this:

```elixir
def update(assigns, socket) do
  {:ok,
    socket
    |> assign(assigns)
    |> assign_rating()
    |> assign_changeset()}
end

def assign_rating(
    %{assigns: %{current_user: user, game: game}} = socket) do
  assign(socket, :rating, %Rating{user_id: user.id, game_id: game.id})
end

def assign_changeset(%{assigns: %{rating: rating}} = socket) do
  assign(socket, :changeset, Survey.change_rating(rating))
end
```

These reducer functions will add the necessary keys to our `socket.assigns`. They'll drop in any `assigns` our parent sends, add a new `Rating` struct, and finally establish a changeset for the new rating.

There are no surprises here. One reducer builds a new rating, and the other uses the `Survey` context to build a changeset for that rating. Now, on to render.

With our socket established, we're ready to render. We'll choose a template to keep our markup code neatly compartmentalized. Create a file, `lib/arcade_web/live/rating_live/form.html.heex`. Add the game title markup followed by the game rating form shown here:

```eex
<div class="survey-component-container">
  <section class="row">
  <h4><%= @game.name %></h4>
  </section>
  <section class="row">
    <.form
      let={f}
      for={@changeset}
      phx-change="validate"
      phx-submit="save"
      phx_target={@myself}
      id={@id}>

      <%= label f, :stars%>
      <%= select f, :stars, Enum.reverse(1..5) %>
      <%= error_tag f, :stars %>

      <%= hidden_input f, :user_id%>
      <%= hidden_input f, :game_id%>

      <%= submit "Save", phx_disable_with: "Saving..." %>
    </.form>
  </section>
</div>
```

The form template is really just a standard Phoenix form, although the syntax for rendering the form may be new to you. The main function in the template is the [`form/1`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Helpers.html#form/1) function: a function component made available by LiveView under the hood. The form function component returns a rendered HEEx template containing an HTML form built with the help of `Phoenix.HTML.Form.form_for/4`.

If you're feeling adventurous, you can [check out the source code for the form function component](https://github.com/phoenixframework/phoenix_live_view/blob/v0.17.5/lib/phoenix_live_view/helpers.ex#L1002). For now, all you really need to know is that calling `form/1` returns an HTML form for the specified changeset, with the specified LiveView bindings. Let's take a closer look at how our form is rendered.

Since the `form/1` function is built on top of the `form_for/4` function, it presents a similar API. Here, we're generating a form for the `@changeset` assignment that was put in assigns via the `update/2` callback.

Then, we bind two events to the form, a `phx-change` to send a `validate` event and a `phx-submit` to send a `save` event. We target our form component to receive events by setting `phx-target` to `@myself`, and we tack on an `id`.

Note that we've set a dynamic HTML id of the stateful component id, stored in socket assigns as `@id`. This is because the game rating form will appear multiple times on the page, once for each game, and we need to ensure that each form gets a unique id. You'll see how we set the `id` assigns for the component when we render it in a bit.

Our form has a `stars` field with a label and error tag and a hidden field for each `user` and `game` relationship. We tie things up with a submit button.

We'll come back to the events a bit later. For now, let's fold our work into the `RatingLive.Index.list/1` function component.

### Render the Component

The `RatingLive.Index.games/1` function component should render the rating form component if no rating for the given game and user exists. Let's do that now.

```elixir
def list(assigns) do
  ~H"""
  <%= for {game, index} <- Enum.with_index(@games) do %>
    <%= if rating = List.first(game.ratings) do %>
      <Show.stars rating={rating} game={game} />
    <% else %>
      <.live_component module={RatingLive.Form}
                        id={"rating-form-#{game.id}"}
                        game={game}
                        game_index={index}
                        current_user={@current_user } />
    <% end %>
  <% end %>
  """
end
```

Here, we call on the component with the `live_component/1` function, passing the user and game into the component as assigns, along with the game's index in the `@games` assignment. We add an `:id`, a requirement of all stateful components. Since we'll only have one rating per component, our `id` with an embedded `game.id` should be unique.

The `live_component/1` function is a function component made available to us by the LiveView framework. It takes in an argument of some assigns and returns a HEEx template that renders the given component within the parent live view. When using `live_component/1` to render a live component, you must specify an assigns of `module`, pointing to the name of the live component module to mount and render, and an assigns of `id`, which LiveView will use to keep track of the component. Also, note the `{}` interpolation syntax we're using — this syntax is required when interpolating within HTML or HEEx tags.

It's been a while since we've looked at things in the browser — but now, if you point your browser at `/survey`, you should see something like this:

![ratings forms](https://blog.appsignal.com/images/blog/2022-01/ratings-forms.png)

### Handle Component Events

We've bound events to save and validate our form, so we should teach our component how to do both. We need one `handle_event/2` function head for each of the `save` and `validate` events. Let's start with `validate`:

```elixir
def handle_event("validate", %{"rating" => rating_params}, socket) do
  {:noreply, validate_rating(socket, rating_params)}
end
```

We need to build the reducer next:

```elixir
def validate_rating(socket, rating_params) do
  changeset =
    socket.assigns.rating
    |> Survey.change_rating(rating_params)
    |> Map.put(:action, :validate)

  assign(socket, :changeset, changeset)
end
```

Our `validate_rating/2` reducer function validates the changeset and returns a new socket with the validated changeset (containing any errors) in socket assigns. This will cause the component to re-render the template with the updated changeset, allowing the `error_tag` helpers in our `form_for` form to render any errors.

Next up, we'll implement a `handle_event/2` function that matches the `save` event:

```elixir
def handle_event("save", %{"rating" => rating_params}, socket) do
  {:noreply, save_rating(socket, rating_params)}
end
```

And here's the reducer:

```elixir
def save_rating(
         %{assigns: %{product_index: product_index, product: product}} = socket,
         rating_params
       ) do
  case Survey.create_rating(rating_params) do
    {:ok, rating} ->
      product = %{product | ratings: [rating]}
      send(self(), {:created_rating, product, product_index})
      socket

    {:error, %Ecto.Changeset{} = changeset} ->
      assign(socket, changeset: changeset)
  end
end
```

Here, we attempt to save the form. On failure, we assign a new changeset. On success, we send a message to the parent live view to do the heavy lifting for us. Then, as all handlers must do, we return the socket.

### Update the Rating Index

What should happen when the game rating successfully saves? The `RatingLive.Index.games/1` function should no longer render the form for that game. Instead, the survey should display the saved rating. This kind of state change is squarely the responsibility of `SurveyLive`. Our message will serve to notify the parent live view to change.

Here's the interesting bit. All the parent needs to do is update the socket. The `RatingLive.Index.games/1` function already renders the right thing based on the content of the assigns that it receives from the parent, `SurveyLive`. All we need to do is implement a handler to deal with the "created rating" message.

```elixir
# lib/arcade_web/live/survey_live.ex
def handle_info({:created_rating, updated_product, product_index}, socket) do
  {:noreply, handle_rating_created(socket, updated_product, product_index)}
end
```

We use `handle_info` so that the parent live view process, `SurveyLive`, can respond to the message sent by the child component. Now, our reducer can take the appropriate action. Notice that the message we match has a message name, an updated game, *and* its index in the `:games` list. We can use that information to update the game list without going back to the database. We'll implement the reducer below to do this work:

```elixir
def handle_rating_created(
         %{assigns: %{products: products}} = socket,
         updated_product,
         product_index
       ) do

  socket
  |> put_flash(:info, "Rating submitted successfully")
  |> assign(
    :products,
    List.replace_at(products, product_index, updated_product)
  )
end
```

The `handle_rating_created/3` reducer adds a flash message and updates the game list with its rating. This causes the template to re-render, passing this updated game list to `RatingLive.Index.games/1`. That function component, in turn, knows just what to do with a game containing a rating by the given user — it will render that rating's details instead of a rating form.

Notice the lovely layering. In the parent live view layer, all we need to do is manage the list of games and ratings. All of the form handling and rating or demographic details go elsewhere.

The end result of a submitted rating is an updated game list and a flash message. Submit a rating, and see what happens:

![rating form success](https://blog.appsignal.com/images/blog/2022-01/rating-form-success.png)

You just witnessed the power of components and LiveView.

## Wrap Up: You've Built a Complex LiveView UI with Components

This post covered a lot of ground. You implemented a sophisticated UI composed from simple layers, all thanks to LiveView components. You can wrap up simple markup with function components, while live components allow you to maintain component state and respond to events. Meanwhile, HEEx templates and LiveView provide some nice semantics for rendering markup and both types of components.

LiveView is growing fast — it's responding to the community's needs and providing even more ergonomic solutions for developing complex interactive single-page apps. Function components and HEEx are just some of the latest features that make LiveView even more enjoyable to use.

Happy coding!

**P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, [subscribe to our Elixir Alchemy newsletter and never miss a single post](/elixir-alchemy)!**