The Primitives of Phoenix LiveView

Episode 7 – The Primitives of Phoenix LiveView

Understanding Phoenix LiveView

In the previous article we saw how to setup Phoenix LiveView, in order to have everything we need to build a simple LiveView gallery app.

Now, before creating our app, I’d like to explore the Phoenix LiveView primitives, understanding the magic behind LiveView while learning how we can build a simple counter.

Once built some confidence and understood how LiveView and its life-cycle work, we’ll see, in the next article, that the gallery app is just an easy evolution of this counter.

New CounterLive module and static HTML

So, let’s now put temporarily aside the GalleryWeb.GalleryLive module we’ve defined during the setup, and create a new live view module GalleryWeb.CounterLive in lib/gallery_web/live/counter_live.ex, which we’ll use in this article mainly as a playground.

# lib/gallery_web/live/counter_live.ex

defmodule GalleryWeb.CounterLive do
  use Phoenix.LiveView

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

  def render(assigns) do
    ~L"""
    <label>Counter: 0</label>
    <button>+</button>
    """
  end
end

and add the live route in the router in lib/gallery_web/router.ex

# lib/gallery_web/router.ex

defmodule GalleryWeb.Router do
  use GalleryWeb, :router
  ...
  scope "/", GalleryWeb do
    ...
    live "/counter", CounterLive  
  end
end

By starting the Phoenix server and visiting the /counter page, we should see on the browser just a label and a button.

label and button

When a client connects to the CounterLive view, mount/3 is the first callback to be invoked. It’s used to setup the view and prepare the data needed for the first render.

Then, the render/1 callback is invoked. In this function we use the ~L sigil to define an inline LiveView template. For small templates I find really convenient to have everything in the same module, but in case of larger templates we can move it to a separate html.leex file.

LiveView templates (LiveEEx) are similar to Phoenix EEx templates, except that they track the changes of the dynamic parts minimizing the data sent to the client.

Great, we have a working live view… but at the moment there are no dynamic parts. The counter <label> has just a static 0 and the <button> does nothing when clicked.

GET request and WebSocket connection

Before going ahead making the view dynamic, let’s take a moment to see in detail the first part of the LiveView’s life-cycle.

Let’s log a string, along with the process id, whenever mount/2 or render/1 are invoked.

# CounterLive
# lib/gallery_web/live/counter_live.ex

require Logger

def mount(_params, _session, socket) do
  Logger.info("MOUNT #{inspect(self())}")
  {:ok, socket}
end

def render(assigns) do
  Logger.info("RENDER #{inspect(self())}")	
  ~L"""
  ...
  """
end

After refreshing the page just one time, we see in the logs that the mount/2 and render/1 are called two times with different pids.

mount/2 and render/1 invoked two times
mount/2 and render/1 invoked two times

What’s happening here?

The browser, to get the http://localhost:4000/counter page, sends an HTTP GET request to the server. The server invokes then mount/2 and render/1 to render the view, sending back the full HTML page.

HTTP GET request

Inside the HTML, we can see our view which is embedded in a <div> container that has some special data attributes (like data-phx-session, data-phx-view…). These attributes will then be used by the LiveView JavaScript library to start a stateful view.

<div data-phx-session="..." data-phx-view="CounterLive" 
     id="phx--1wl284P">

  <label>Counter: 0</label>
  <button>+</button>

</div>

By answering to a HTTP GET request with a fully rendered page, we can support clients that do not necessarily run JavaScript, which makes LiveView also great for SEO.

When the browser receives and loads the page, it also loads the JavaScript we wrote during the setup in assets/js/app.js

// assets/js/app.js
...
let liveSocket = new LiveSocket("/live", Socket)
liveSocket.connect()

and connects to the server with a websocket. The Counter LiveView process will then track the changes, pushing the updated dynamic values to the client.

Once the browser and the server are connected via websocket, mount/2 is invoked again to setup the data, and render/1 to re-render the view. This second time, only the view’s HTML is sent back to the browser

Browser and Server connected via WebSocket
Browser and Server connected via WebSocket

Make it dynamic with assign/3

Let’s now make the counter value, in the <label> tag, dynamic. In the mount/2 callback, we assign/3 a counter value to the socket

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

and in the template, in render/1, we change the fixed number with <%= @counter %>.

def render(assigns) do
  ~L"""
  <label>Counter: <%= @counter %></label>
  <button>+</button>
  """
end

After refreshing the page we get obviously the same result, but by changing the :counter value we should see that this change is reflected on the browser as well.

Rendering a different @counter value
Rendering a different @counter value

Phoenix bindings, handle_event/3 and update/3

The easiest way to add some user interaction is with the help of Phoenix Bindings which are special attributes to add to HTML elements in the LiveView template. In this example, we use the click event binding, adding phx-click="event name" attribute to the button element.

<button phx-click="incr">+</button>

When we click the + button, the browser sends via the websocket an "incr" event to the CounterLive view process running on the server.

Let’s try it on the browser, by refreshing and clicking the + button.

LiveView process crash
LiveView process crash

We see a red background because the CounterLive process is crashed. Looking at the logs on the terminal, is clear that we need to implement the handle_event/3 callback.

LiveView process crash logs
defmodule CounterLive do
  def handle_event("incr", _event, socket) do
    ...
  end
end

The first argument matches the "incr" string in phx-click, the second gives details about the event and the third is the socket.

In handle_event/3 we want to increment the counter, but how can we access to the current counter value?

Let’s inspect the socket when the mount/2 callback is invoked

def mount(_params, _session, socket) do
  socket =
    socket
    |> assign(:counter, 0)
    |> IO.inspect()
    {:ok, socket}
end

Refreshing the page, we see on the terminal the Phoenix.LiveView.Socket struct.

#Phoenix.LiveView.Socket<
  
  assigns: %{counter: 0},
  changed: %{counter: true},
  
  endpoint: GalleryWeb.Endpoint,
  id: "phx-rf-5Qdcj",
  parent_pid: nil,
  view: GalleryWeb.CounterLive,
  ...
>

The assign/3 function sets the :counter value in the assigns map and flags the value as changed in the changed map.

So, to increment the counter, we could assign a new value socket.assigns.counter + 1

socket = assign(socket, :counter, socket.assigns.counter + 1)

and it would work. But, when updating a value, I prefer to use the update/3 where we pass a function &(&1 + 1) that increments the counter by 1.

def handle_event("incr", _event, socket) do
  socket = update(socket, :counter, &(&1 + 1))
  {:noreply, socket}
end

The handle_event/3 callback then returns the new socket in the tuple {:noreply, socket}.

This is everything we need to do – we don’t need to update the front-end elements our self – all this is taken over by LiveView!

Let’s refresh and try the counter! By clicking + we now see on the browser that the counter value increases 🎉

Working LiveView Counter
Working LiveView Counter

How LiveView updates the counter on the browser

It really feels like magic: just incrementing the counter in handle_event/3, LiveView takes care of all the rest, from propagating the changes to updating the <label> tag.

How does LiveView manage these updates under the hood?

Click event -> counter update -> re-render -> update sent back to the browser
Click event -> counter update -> re-render -> update sent back to the browser

Browser and server are connected via a websocket connection. When we click the + button an "incr" event is sent to the CounterLive process.

We can see, using the browser inspector under network, the websocket connections and the messages exchanged with the server.

Increment event message
Increment event message

The LiveView process receives the "incr" event and it invokes our handle_event/3 implementation, updating the counter.

Every time we update or assign a value, LiveView re-renders the view, sending back only the updated dynamic values.

Updated dynamic values are sent back to the browser
Updated dynamic values are sent back to the browser

What the server pushes down to the client is just the updated counter value, with some metadata. It means that if we have a much larger template, LiveView (thanks to LiveEEx) tracks the changes happened to the template’s dynamic parts and sends just the updated values to the browser.

When the browser receives updates from the server, it uses the patrick-steele-idem/morphdom JavaScript library to patch the DOM. Using the inspector we see that only the content in the <label> tag is updated.

DOM updates
DOM updates

What’s next

In this article we’ve seen the mount/3, render/1, handle_event/3 callbacks and LiveView’s life-cycle, inspecting the messages and understanding a bit of the magic behind LiveView.

We have now all the elements we need to build, in the next article, our gallery app! 👩‍💻👨‍💻