Modern Talking with Elixir: Messenger App Tutorial with Phoenix LiveView

Modern Talking with Elixir: Messenger App Tutorial with Phoenix LiveView

Why Phoenix?

The thing about Elixir, on top of Erlang/OTP, is that it offers a great mix of making life easy and being a scalable and reliable platform that will not let you down when your estimated traffic of 4000 users becomes 4,000,000 users.

Phoenix Framework is Elixir's answer to the never-ending question of how to build rich web applications, and it's got a lot of tools that make the job easy - one of the latest being Phoenix LiveView.

Long story short, LiveView is a tool that lets an Elixir developer create reactive UIs without writing a single line of JS code. Which is great, given that many Elixir developers do not exactly consider themselves fluent at JS, or - just like myself - are not exactly in love with JS.

Lessons learnt from reactive UI libraries

Many JavaScript frameworks, both contemporary and not-so-contemporary ones, rely on manipulating the page's DOM for dynamic content updates.

Historically, for instance, developers using BackboneJS would define a Backbone.View to represent an atomic chunk of user interface, behind which there's a Backbone.Model, encapsulating the business logic of data.

Backbone remained unopinionated about how views were to be rendered, so it had no built-in tools to make the re-rendering of views on model changes efficient - the whole structure of a view had to be built from scratch and replaced, which tended to yield inefficient views.

In contrast, modern frameworks such as ReactJS or Vue.js don't care about how the data model layer works at all (loosely coupled data stores such as Redux are often used for this) - but they have a virtual DOM concept - long story short, a pattern of incrementally upgrading only those elements that need to be changed, based on changes in the state of particular components and their children.

The challenge, though, is pretty much down to how to exchange data between the UI and the backend. You will usually need to implement a JSON API or a GraphQL service, or perhaps you could develop a WebSocket-based solution using Phoenix Channels.

Either way, the Pareto 80/20 principle will imminently catch you, and when you get to the 20% of work needed to finish off your message-passing code, it'll soon become a framework within a framework.

Why, where & how LiveView excels

Phoenix LiveView's concept is both groundbreaking and familiar, in different ways.

It is familiar in that it lets you define UI elements as nestable components composed of pure HTML markup, and it builds upon the experience of reactive UI frameworks in implementing mechanisms that calculate diffs between consecutive UI states to ensure efficient updates.

It is groundbreaking in the way it maintains the states of components and manages their updates - in Phoenix LiveView, components are stateful on the server, and their events and updates are communicated via a bidirectional WebSocket connection.

Phoenix LiveView is built on top of Elixir processes and Phoenix Channels - every LiveView instance is a BEAM process, acting very much like a GenServer, receiving messages and updating its state.

While modern JS frameworks such as React have server-side rendering capabilities, it is usually not convenient to do this in a non-NodeJS backend server. Rendering content via JavaScript often results in SEO issues, and some trickery is needed for search engines to index the page correctly. In Phoenix LiveView, the initial render is static as in the classic HTML request-response cycle, so you'll get good Lighthouse scores and it won't hurt your SEO.

Erlang easily maintains thousands of processes concurrently, and Phoenix authors have even managed to make it handle 2 million WebSocket connections on a single (albeit pretty strong) machine. With the server using Elixir's strengths to manage LiveView states, the client-side logic can be thin and simple.

In fact, as stated in the introduction, in most LiveView-powered apps you won't write a single line of JS code. In many cases, when interacting with an element whose update is supposed to fetch data for a new UI state from the server, the workflow using a reactive JS framework would be:

  1. Handle the element's change event
  2. Send a request to the server containing the actual changes
  3. Receive response and update state store based on response data
  4. Let the view layer re-render the changed DOM elements

This involves annotating HTML elements so that they can be identified by JS code, writing browser-side scripts to handle the element's state change event, send a payload to the server, which processes the request as part of a Phoenix controller action.

With Phoenix LiveView, you only write HTML and Elixir code, with the JS part being handled by a script bundled with the LiveView package.

Phoenix LiveView basic usage

The basic idea behind Phoenix LiveView is very simple and straightforward. Be sure to subscribe to our newsletter to learn more!

LiveView is an Elixir behaviour, and your most basic LiveView definition will consist of two callback implementations:

  • A render/1 function, containing the template of how your component is represented in HTML, with elements of the component's state interpolated. This is much like defining an ordinary view. The special ~L sigil is used to interpolate assigns into your EEx syntax, and convert it into an HTML-safe structure.
  • A mount/2 function, wiring up socket assigns and establishing the LiveView's initial state.
  defmodule YourappWeb.CounterLive do
    use Phoenix.LiveView

    def render(assigns) do
      ~L"""
      <a href='#' phx-click='increment'>
        I was clicked <%= @counter %> times!
      </a>
      """
    end

    def mount(params, socket) do
      {:ok, assign(socket, :counter, 0)}
    end
  end

However, the whole fun of using LiveView is managing its state, and the next two callbacks will come in handy.

  • A handle_event/3 function, handling events coming from the browser. Noticed the phx-click attribute in our template's link? This is the name of an event that will be transported to the LiveView process via WebSockets. We'll define a function clause that will match to the event's name.
  def handle_event("increment", params, %{assigns: %{counter: counter}} = socket) do
    {:noreply, assign(socket, :counter, counter + 1)}
  end

It will mutate the LiveView's state to have a new, incremented value of the counter, and the render/1 function will be called with the new assigns.

The second argument, here named params, is of special interest as well, because - in the case of a phx-click event - it contains the event's metadata:

  %{
    "altKey" => false,
    "ctrlKey" => false,
    "metaKey" => false,
    "pageX" => 399,
    "pageY" => 197,
    "screenX" => 399,
    "screenY" => 558,
    "shiftKey" => false,
    "x" => 399,
    "y" => 197
  }

We trust that you won't now hesitate to try it out with a <form> tag and a phx-change attribute to see what event metadata are passed when a form element's value is changed. Either way, we'll explore this in more detail in later episodes of this tutorial - stay tuned and subscribe to our newsletter so that you don't miss out!

  • A handle_info/2 callback, handling events coming from anywhere but the browser. This means events sent from external sources (remember a LiveView is just an Elixir process, so you can do whatever's needed in order for it to receive messages!), or events sent from the LiveView to itself. For instance, it takes this to increment the counter every 5 seconds:
  def mount(params, socket) do
    if connected?(socket), do: :timer.send_interval(5000, self(), :increment)

    {:ok, assign(socket, :counter, 0)}
  end

  def handle_info(:increment, %{assigns: %{counter: counter}} = socket) do
    {:noreply, socket |> assign(:counter, counter + 1)}
  end

To reduce code repetition, you could make handle_event/3 send a message to self() that triggers the same handle_info/2 routine.

You can now access your LiveView as a standalone route - to do this, put this in your router.ex:

import Phoenix.LiveView.Router

scope "/", YourappWeb do
 live "/counter", CounterLive
end

...or render the LiveView within any other template:

<%= Phoenix.LiveView.live_render(@conn, YourappWeb.CounterLive) %>

The Curious Messenger Roadmap

We'll make you familiar with how to wield the Phoenix LiveView sword, and you'll build a fully-fledged Messenger replacement, which will make you (almost) forget about any other instant messaging app you had ever used before...

Phoenix LiveView is obviously only part of the story, and there's a lot more ground that we'll cover. We'll do a few episodes, each of which touches a different set of concerns that we'll have to consider when designing the app.

  • At the beginning, we'll bootstrap the app and install all needed dependencies, design the app's database and context structure, with all of the app's business logic in mind.
  • Then we'll implement the app's user authentication feature using Pow, a great library integrating all the tools you need to let users sign up and log into the application.
  • Next, we'll go on to implement the actual awesome Curious Messenger features, and here's where most of the Phoenix LiveView magic will shine. We'll show you how to create a live-updated view of your contact list and of each of your conversations.
  • Obviously, we'll also elaborate on what can go wrong when using LiveView, because the worst assumption one can make is that people's network connections are perfect. We'll see for ourselves that Phoenix LiveView is not the Holy Grail of reactive UI building solutions and that this approach has several shortcomings that need to be kept in mind.
  • Finally, we'll fine-tune the Curious Messenger app, adding some customizable settings and push notifications (did we actually say there'll be no JS? We lied.) so that you never miss out on any message from your Curious Friends.

Keep #BusyBeingCurious - hopefully you weren't too bored with our introductory talk. The real thing is coming very soon - stay tuned and subscribe!