The Pragmatic Studio

The Lifecycle of a Phoenix LiveView

August 10, 2022

In the previous video we built a basic Phoenix LiveView from scratch to see how to react to user events. Now let’s walk through the lifecycle of a LiveView in this video from our free course. The video has a deep-dive exploration of what’s on the wire that you won’t want to miss!

Initial Stateless HTTP Request

When we browse to the light page, that sends a regular HTTP GET request to the server. And we have a live route declared in the router for that:

live "/light", LightLive

Then in our LightLive module, the mount callback is invoked which assigns the initial state to the socket. And as you’ll recall, it’s a brightness of 10:

def mount(_params, _session, socket) do
  socket = assign(socket, :brightness, 10)
  {:ok, socket}

Then render is automatically invoked with the state that mount assigned to the socket:

def render(assigns) do
  <h1>Front Porch Light</h1>
  <div id="light">
    <div class="meter">
      <span style={"width: #{@brightness}%"}>
        <%= @brightness %>%

    <button phx-click="off">
      <img src="images/light-off.svg">

    <button phx-click="down">
      <img src="images/down.svg">

    <button phx-click="up">
      <img src="images/up.svg">

    <button phx-click="on">
      <img src="images/light-on.svg">

As a result, a full HTML page is sent back to the client as a regular HTTP response:

  <div class="meter">
    <span style="width:10%">

Handling the initial request this way has a few important benefits:

  1. the response is super-quick

  2. you get a complete, fully-rendered, and meaningful HTML page (not just a shell of a page) even if JavaScript is disabled in the browser

  3. a LiveView page is search-engine-friendly

OK, nothing too surprising so far, but here’s where things get interesting…

Mount with Stateful LiveView Process

When the initial page is loaded, it also loads the JavaScript in assets/js/app.js which turns around and opens a persistent websocket connection to the server:

const liveSocket = new LiveSocket("/live", Socket, {
  params: { _csrf_token: csrfToken },


It’s at this point that a stateful LiveView process is spawned. mount is then invoked again (this time inside of the stateful process!) and initializes the state of that process by assigning values to the socket.

Then, as you probably already guessed, render is also invoked again to render a new view for that state. And the new view is pushed back to the browser over the websocket. But what gets sent back isn’t a string of HTML as you might expect. It’s actually something a bit more intriguing.

So let’s zoom in and focus on this section of our LiveView template:

<div class="meter">
  <span style={"width: #{@brightness}%"}>
    <%= @brightness %>%

Since brightness is interpolated in two EEx tags in this template, we have two dynamic values—values that may or may not change:

<%= @brightness %>
<%= @brightness %>

The rest of the template is static—it will never change:

<div class="meter">
  <span style="width: %">

So LiveView splits this template into two parts: the stuff that’s dynamic and the stuff that’s static.

Both of the dynamic values evaluate to 10, since that’s the initial brightness. You can think of the first value as being in position (or index) 0 of the template and the second value being in position (or index) 1 of the template:

0: 10
1: 10

Having split the template into static and dynamic parts, these parts are then sent to the client. Then the JavaScript provided by the LiveView library weaves (zips) the static and dynamic parts together.

Reacting to Events!

Splitting the rendered content into static and dynamic parts really pays off when the LiveView starts handling events. For example, when we click the button to turn the light on, an on event is pushed down the websocket to the LiveView process and gets handled by a matching handle_event callback:

def handle_event("on", _, socket) do
  socket = assign(socket, :brightness, 100)
  {:noreply, socket}

A brightness of 100 is assigned to the socket. And whenever a LiveView’s state changes, the render function is automatically called. Since handling the on event only changed the brightness value (setting it to 100), only the two EEx tags in the template need to be re-evaluated:

<div class="meter">
  <span style={"width: #{@brightness}%"}>
    <%= @brightness %>%

If our template had other EEx tags that interpolate other dynamic values, they would only be re-evaluated if turning on the light changed them.

That’s pretty clever! Think about it: LiveView doesn’t have to re-evaluate the code in all the EEx tags in the template. It only has to re-evaluate the code for things that changed. If you’re curious, this is made possible because LiveView templates get compiled to Elixir code.

So what does LiveView send to the browser this time? Well, it doesn’t send the static part again—that’s already cached in the browser. Only the new dynamic values and their indexes get sent over:

0: 100
1: 100

So as before, all the LiveView JS needs to do is weave (zip) the static and dynamic parts together, and then it uses the morphdom library to efficiently patch the DOM to turn the light on.

Tracking Changes

What happens if we click the on button again? Well, an on event is pushed down the websocket, the handle_event callback assigns a brightness of 100, and render is invoked. But the LiveView template is smart: it tracks changes to the state and it knows that the brightness value hasn’t actually changed. It was 100 before, and its 100 now.

And since nothing changed, there’s no need to send new dynamic values to the client. In other words, the template does diff tracking. And so the response has no static or dynamic values!

Now if we click the off button, an off event is sent and handled, which sets the brightness to 0, and render is invoked. And since the LiveView template is tracking changes to state, it recognizes that the brightness changed. So all it sends back to the client are these dynamic values that were modified.

So that’s what happens at a conceptual level. Watch the video to see us break down everything that’s on the wire—static and dynamic parts—throughout the lifecycle!

Enabling Debugging

LiveView has a built-in way to see what’s on the wire. In assets/js/app.js it exposes the liveSocket instance on window:

window.liveSocket = liveSocket;

So if you open the “Console” tab in the browser’s devtools, you can turn on debugging like so:


When you enable debugging, a flag is dropped in the browser’s sessionStorage so debugging stays enabled for the duration of the browser session.

As you probably guessed, you can disable debugging using disableDebug().


Hopefully this gives you a better (deeper!) understanding of the lifecycle of a LiveView. It’s really ingenious. But more than just being cool, knowing what goes on behind the scenes puts you in a better position to get the most out of LiveView.

🔥 Learn LiveView In Our Free Course!

Join us us as we build 25 practical LiveView examples that you can drop right into your own app. By seeing LiveView used in many different situations, you'll quickly build up a deep intuition for when and how to use it.

LiveView Course