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:
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.
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.
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.
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.
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
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
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
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:
attrs
: The props
our component needs and acceptsslots
: A special prop type that accepts HTML markup and different componentsThe 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.
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!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSimplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
Explore how to integrate TypeScript into a Node.js and Express application, leveraging ts-node, nodemon, and TypeScript path aliases.
es-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.