How to write reusable components in Phoenix LiveView

Phoenix is an HTTP routing framework for the Elixir language that allows you to create endpoints that return either JSON or HTML. There are several other types that you can return, but JSON and HTML are the most common. In this article, we’ll explore Phoenix by learning how to write reusable components in Phoenix LiveView.

Jump ahead:

What is a LiveView?

LiveView is a special type of connection that we can return from the JSON and HTML routes mentioned above. Instead of returning static HTML or JSON, we return a WebSocket connection that allows the frontend and backend to communicate, hence the “live” in LiveView.

A LiveView is a server-side, stateful pattern that keeps frontend state on the server. In the context of an Elixir application, a single connected user holds a process on the server where the state is kept and updated. When compared to other frontend libraries and frameworks like React and Svelte, where all state is kept in JavaScript on the frontend itself, this might seem counterintuitive. Therefore, we’ll need to take on a different mindset.

How does a LiveView fit into the frontend?

Since all state is kept on the server, a button click doesn’t necessarily call a JavaScript function or update the state. Rather, when the button is clicked, an event is sent over the WebSocket connection. The backend will handle that event, update the state, and return the new HTML to be rendered on the frontend.

In essence, a Phoenix LiveView is a single-page application. Therefore, even when performing routing, you remain on the same page. The routing comes with a new HTML payload over the WebSocket connection, and some JavaScript code will patch the current view, much like how React works today.

Components in Phoenix

Now that we know what a LiveView is about, let’s see what one actually looks like. In my experience, I tend to create components that allow me to encapsulate markup and styling for consistency no matter how small the project I’m working on. The ability to do so is something I look for when deciding to learn a new framework or library, helping to keep a consistent look and feel on my sites and making it easy to create layouts with the same amount of padding and margin.

In my opinion, the component model used by React, which allows users to create small components, is the bees-knees. In LiveView v0.18, we can get very close to that.

Phoenix component types

We’ll start with a basic view that contains some layout and a button. Then, we’ll learn how to make our main markup cleaner by abstracting parts out into separate components. Finally, we’ll make the components safer to use with compile time warnings, providing them with sensible defaults but allow for overriding.

LiveView

A LiveView is usually the root of a page. The biggest difference between Phoenix LiveView and React is that React typically has only one root. From there, we render a router, which is essentially just a large switch case. That then renders different components.

In an Elixir LiveView, usually, each route is independent and has nothing functionally in common with other LiveViews. But, this would make for a terrible foundation to build applications on, so there are exceptions to the rule that are beyond the scope of this article.

For now, we just need to think of a LiveView as a container in which our data fetching happens. The code below shows how we define a live_view:

defmodule LogRocketWeb.PostsLive.Index do
  use LogRocketWeb, :live_view
  ...
end

Live component

If the live_view is the data container, the live component is the stateful component. Therefore, a live component can initialize its own state, keep track of it, and separate to all other live_components.

We could materialize this in a list of counters where each counter component keeps a record of its own count without needing to collect it in the LiveView container:

defmodule LogRocketWeb.PostsLive.FormComponent do
  use LogRocketWeb, :live_component
  ...
end

Component

On the other hand, a component is a pure markup component that can still take some props to render. It is this component type, which is fairly new in the Phoenix ecosystem at the time of writing, that really empowers us to create reusable components through composition:

defmodule LogRocketWeb.CoreComponents do
  use Phoenix.Component

    def button(assigns) do
      ~H"""
      <button>
        Click me
      </button>
      """
    end
end

Writing a LiveView component

Now that we’re familiar with the different component types, let’s get to writing our components. Since I’ve already talked about the button, that’ll be our initial component.

When writing components, a lot of the work is redundant. Usually, we begin writing low-level components just as extensions of existing HTML elements but with a styling that makes them consistent throughout our application.

A LiveView component contains the following four things:

  • A function: We’ll use a function to call and use our component
  • attrs: The props our component needs and accepts
  • slots: A special prop type that accepts HTML markup and different components
  • Tailwind CSS: We’ll include Tailwind CSS for styling

The Elixir compiler will warn us if we use a component with a missing required field. Therefore, we get compile time warnings about misuse and can catch errors during development time.

The code below shows a simple button component that I’ll dissect to explain the vocabulary for our component structure:

  attr :type, :string, default: nil
  attr :class, :string, default: "text-white bg-blue-600 hover:bg-blue-800 rounded p-3"
  attr :rest, :global, include: ~w(disabled form name value)
  slot :inner_block, required: true
  def button(assigns) do
    ~H"""
    <button
      type={@type}
      class={@class}
      {@rest}
    >
      <%= render_slot(@inner_block) %>
    </button>
    """
  end

  attr :title, required: true
  slot :inner_block
  def container(assigns) do
    ~H"""
    <div class="flex flex-col gap-3 p-3">
      <h1> <%= @title %> </h1>
      <%= render_slot() %>
      <button>
        Next page
      </button>
    </div>  
    """  
  end

Let’s start by looking at the container function, which returns some markup that is a title passed in as a prop. This is denoted by the attr above the function. For basic content, a slot is denoted by the slot prop.

In the markup, we render a button that, figuratively, goes to the next page. Using the same knowledge, let’s take a look at the button.

Although much of it is the same, there is a new global attribute that will take any part of the HTML standard and accept it as props. The {@rest} in the markup for the button will then spread the props out on the button component. Here, the component becomes easily extendable, allowing us to define some defaults so we can solve the specific problem at hand.

Now that we know what a component looks like, let’s learn how to use one. Following tradition, let’s write ourselves a counter:

defmodule LogrocketWeb.Counter do
  use LogRocketWeb, :live_view

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

  def handle_event("inc", _params, socket) do
    {:noreply, assign(socket, count: socket.assigns.count + 1)}
  end

  def render(assigns) do
    ~H"""
    <div>
      <h1>Counter</h1>
      <p>Count: <%= @count %></p>

      <button phx-click="inc">Increment</button>
    </div>
    """
  end
end

In its entirety, this small file is a webpage with a counter and a button. Notice how in the mount function, we return {:ok, assign(socket, count: 0)}, telling Phoenix that our component mounted correctly and holds a single point of state called count.

Clicking on the button in the markup calls the handle_event function, which then returns a {:noreply, assign(…)} tuple.

Now, imagine that we have 42 different pages with similar counters, and we change our theme to be all blue. We’d have to go into all of those files and change the classes. I think we can do better. To start off, let’s refactor our button to use the button component we created earlier:

defmodule LogrocketWeb.Counter do
  use LogRocketWeb, :live_view

  ...

  def render(assigns) do
    ~H"""
    <div>
      <h1>Counter</h1>
      <p>Count: <%= @count %></p>
      <LogRocketWeb.Components.button phx-click="inc">Increment</LogRocketWeb.Components.button>
    </div>
    """
  end
end

The only change we need to implement is to reference the module in which we wrote said button. Since we used the global attribute, the phx-click event handler is automatically passed down to the underlying button. Our Increment text is passed down as our inner_block.

To ensure consistency between the pages, all of our counter pages need to follow a specific flex column layout. This leads us into the next refactor, which is to use our container component to streamline our counter layouts:

defmodule LogrocketWeb.Counter do
  use LogRocketWeb, :live_view

  ...

  def render(assigns) do
    ~H"""
    <LogRocketWeb.Components.container title="Counter">
      <div>
        <p>Count: <%= @count %></p>
        <LogRocketWeb.Components.button phx-click="inc">Increment</LogRocketWeb.Components.button>
      </div>
    </LogRocketWeb.Components.container>
    """
  end
end

The code above follows the same basic principle. It takes the title as a prop and puts it where we specified in the earlier markup. Everything we put in the div is passed into the inner_block slot.

Conclusion

In this article, we explored the Phoenix framework by learning what a basic LiveView component looks like and how to add our own reusable components.

We’ve only scratched the surface of what is possible with Phoenix components, but I hope this will serve as a great example of how to build your own app’s basic components, ensuring that your users have a smooth and consistent experience when using your app. Be sure to leave a comment if you enjoyed this article or if you have any questions. Happy coding!


More great articles from LogRocket:



Source link