Phoenix.LiveView: Interactive, Real-Time Apps. No Need to Write JavaScript.


Phoenix LiveView

Phoenix LiveView is an exciting new library which enables rich, real-time user experiences with server-rendered HTML. LiveView powered applications are stateful on the server with bidrectional communication via WebSockets, offering a vastly simplified programming model compared to JavaScript alternatives. While modern JavaScript tooling enables sophisticated client applications, it often comes at an extreme cost in complexity and maintainability. There’s a common class of applications where rich experiences are needed, but full single-page applications are otherwise overkill to achieve the bits of required rich interaction. This applies to broad use-cases, including simple real-time updates, client-side style validations with immediate feedback, autocomplete inputs; and it can go as far as real-time gaming experiences. LiveView fills this gap and challenges what’s possible with server-rendered applications. Here’s a sneak peek at what it’s capable of:

As an application developer, you don’t need to write a single line of JavaScript to create these kinds of experiences. You can write and test all your code in a single language: Elixir. Let’s find out how.

Programming Model

Live views share functionality with the regular server-side HTML views you are used to writing – you write some template code, and your render function generates HTML for the client. That said, live views go further by enabling stateful views which support bidrectional communication between the client and server. Live views react to events from the client, as well as events happening on the server, and push their rendered updates back to the browser. In effect, we share similar interaction and rendering models with many client-side libraries that exist today, such as React and Ember. If we implemented a Theromstat display with temperature control, the programming model could be illustrated as follows:

A live view starts its life as a stateless render from the controller, which sends HTML to the client as a regular HTTP request. Even with JavaScript disabled, end-users and crawlers alike will receive HTML as expected. After the HTTP request, the live view connects to the server and is upgraded to a stateful process. It then awaits events from the client or state changes on the server. Just like many client-side frameworks, any time a callback changes the view’s state, render is re-invoked and the updates are applied to the browser’s DOM. This process repeats as long as the user is visiting the page. Let’s check out our ThermostatView in detail:

defmodule DemoWeb.PageController do use DemoWeb, :controller def thermostat(conn, _) do live_render(conn, DemoWeb.ThermoStatView) end
end defmodule DemoWeb.ThermostatView do use Phoenix.LiveView import Calendar.Strftime def render(assigns) do ~L""" <div class="thermostat"> <div class="bar <%= @mode %>"> <a phx-click="toggle-mode"><%= @mode %></a> <span><%= strftime!(@time, "%r") %></span> </div> <div class="controls"> <span class="reading"><%= @val %></span> <button phx-click="dec" class="minus">-</button> <button phx-click="inc" class="plus">+</button> </div> </div> """ end def mount(_session, socket) do if connected?(socket), do: Process.send_after(self(), :tick, 1000) {:ok, assign(socket, val: 72, mode: :cooling, time: :calendar.local_time())} end def handle_info(:tick, socket) do Process.send_after(self(), :tick, 1000) {:noreply, assign(socket, time: :calendar.local_time())} end def handle_event("inc", _, socket) do {:noreply, update(socket, :val, &(&1 + 1))} end def handle_event("dec", _, socket) do {:noreply, update(socket, :val, &(&1 - 1))} end def handle_event("toggle-mode", _, socket) do {:noreply, update(socket, :mode, fn :cooling -> :heating :heating -> :cooling end)} end
end

When our live view mounts, we check to see if it’s being statically rendered, or if it has connected back to the server. If we are connected, we send ourself a :tick message every second, which triggers handle_info, a callback used to handle messages within the server. Here we update our :time to the current time. Next, we use phx-click bindings containing an event name in the template. Those bindings emit a client event that can be handled in handle_event/3 callbacks. When we receive an event to increment or decrement our temperature, we simply update our socket’s state. Likewise, when we receive a "toggle-mode" event, we simply update our mode assign to the flipped setting. If any callback changes the socket’s state, LiveView detects the change, calls render/1 with the updated assigns, and pushes the results to the browser. Let’s see it in action:

Server-Rendered HTML️™ – Can It Scale?

We’ve heard for years that server-rendered HTML is a thing of the past for rich applications, and besides, how fast can an application be if it has to render the entire template on the server on every change? Fear not, LiveView shatters these misconceptions and often sends less data than an equivalent client-rendered application.

Thanks to José Valim’s excellent work on LiveEEx, a template language for embedded Elixir capable of computing diffs, we are able to send only the parts of the page which have changed, and we never send the static parts of the template after the initial render. This allows the browser to receive all static and dynamic parts on mount, then only the dynamic parts that have changed on update. When the browser receives an update of dynamic parts of the page, it computes the HTML from its static cache and performs a minimal DOM patch with morphdom. This enables both minimal updates over the network and minimal updates to the DOM. We achieve this by compiling the template to code blocks that only execute the dynamic parts of the template conditionally:

Let’s see this in action with another LiveView example – a simple real-time image editor:

full source

The DOM inspector highlights the areas of the DOM as they are updated. We can see that even with our ImageView updating the width at the resolution of the browser onchange event, we patch only the minimal bits of the DOM which have changed, such as the displayed width and the image tag’s width. Likewise, when we view the JavaScript console with debug logging enabled, we see, despite the server calling our render/1 function on every change, the only data sent down the wire is the minimal dynamic values required to update the page. Amazing! It gets even better though – instead of sending all dynamic values if there is a change, LiveView will only send the updated dynamic values that changed. You can see this in action at the end of the clip where a toggle of the image background color does not send the unchanged width and vice-versa. These optimizations allow us to match single-page applications in payload sizes, as we’ll see in the next example.

Reduced Complexity With Optimal Data Transfer

One of the most enjoyable aspects of LiveView is how little code is required to accomplish your features versus the JavaScript code, tooling, and related server code to accomplish the same task. In many cases, we also match single-page applications in terms of data transfer, and in some cases send less data than an equivalent SPA. One common use-case that illustrates this well is autocomplete of inputs. What would otherwise require JavaScript, a server HTTP fetch endpoint, encoding logic, and client template code, is 35 LOC total for LiveView:

defmodule DemoWeb.SearchView do use Phoenix.LiveView def render(assigns) do ~L""" <form phx-change="suggest" phx-submit="search"> <input type="text" name="q" value="<%= @query %>" list="matches" placeholder="Search..." <%= if @loading, do: "readonly" %>/> <datalist id="matches"> <%= for match <- @matches do %> <option value="<%= match %>"><%= match %></option> <% end %> </datalist> <%= if @result do %><pre><%= @result %></pre><% end %> </form> """ end def mount(_session, socket) do {:ok, assign(socket, query: nil, result: nil, loading: false, matches: [])} end def handle_event("suggest", %{"q" => q}, socket) when byte_size(q) <= 100 do {words, _} = System.cmd("grep", ~w"^#{q}.* -m 5 /usr/share/dict/words") {:noreply, assign(socket, matches: String.split(words, "\n"))} end def handle_event("search", %{"q" => q}, socket) when byte_size(q) <= 100 do send(self(), {:search, q}) {:noreply, assign(socket, query: q, result: "…", loading: true, matches: [])} end def handle_info({:search, query}, socket) do {result, _} = System.cmd("dict", ["#{query}"], stderr_to_stdout: true) {:noreply, assign(socket, loading: false, result: result, matches: [])} end
end

In this example, we have a dictionary search with autocomplete suggestions. We are taking advantage of the datalist tag, which can be used for browser-native autocomplete (requires a polyfill in Safari). For this demonstration, we simply shell out to grep using the system’s built-in word dictionary. To trigger a suggestion update, we can simply add phx-change="suggest" to the parent form, which executes our handle_event("suggest", .., ...) callback on every keystroke. Our callback’s job is to update the socket assigns with the new matches. If new matches exist, render/1 is invoked, and the client will receive and render the minimal diff as we’ve seen. Likewise, on phx-submit,we trigger the handle_event("search", .., ...) callback, which shells out to the dict command line dictionary. Since dict makes a remote network call for word lookup, we async this operation by sending ourselves a message, and update our assigns with a loading state. After picking up our {:search, query} message in a familiar handle_info callback, we perform the fetch and update our socket assigns with our search result and loading state - which, you guessed it, calls render/1 and sends only the values which have changed.

This is a shockingly small amount of code compared to what is required to support a single-page application, even in the most ideal circumstances. Likewise, if we inspect the data on the wire during keystrokes, we can see how LiveView matches an equivalent hand-written client applications in data transfer:

Because we only send the dynamic values in their final representation for UI display, in some cases we can send less data than even the most optimized client templated application. Another benefit is the fact that LiveView is powered by Phoenix Channel WebSockets, providing less latency than the AJAX triggered HTTP requests often used for autocomplete suggestions. In AJAX, we would have the overhead of the HTTP request and response, the work on the server to authenticate every request, etc. Here, because it is stateful, we reduce the latency and server work, emitting JSON payloads that would be comparable to a hand-written JSON response. In future versions, we will send even less data using the binary Erlang Term Format, which should cut the payload size in half for examples like this.

Error Recovery on a Battle-Tested Foundation

LiveView sits atop Erlang’s battle-tested foundations for not only building stateful applications, but handling their failure when things go wrong. Using these battle-tested primitives, all Phoenix live views have built-in error handling and recovery. You can even customize the UI error states without touching a single line of JavaScript, thanks to the phx-error and phx-disconnected classes that are automatically applied to your templates. Let’s see it in action by imagining we introduce the following bug in our ThermostatView:

 def handle_event("inc", _, socket) do if socket.assigns.val >= 75, do: raise "boom" {:noreply, update(socket, :val, &(&1 + 1))} end

Now let’s see how it recovers:

One of the defining lessons of Elixir and Erlang is once a process crashes, it should be restarted in its known good state. We can see this in action with our thermostat. As soon as the view crashes on the server, we see an error and loading state applied to the client, via phx-errorand phx-loading CSS classes. Next, we quickly see the client re-establish its ThermostatView process on the server with its known good initial state, and the client becomes avaiable for continued interaction. The beauty of this platform is the same error recovery ideas apply just as well to distributed systems as they do to client UIs. A user should never be left in an unrecoverable error state due to a bug. The stateful systems we build in Elixir recover to known good conditions and continue, just as they should.

LiveView Can Do More Than You Think

Thanks to Elixir, which powers Phoenix’s legendary microsecond response times, and Erlang OTP’s stateful foundations, LiveView is perfectly suited to render templates and handle callbacks at high load and frequencies. I covered this in detail during my ElixirConf 2018 Keynote by showing a rainbow animation powered by the server at 60FPS.

But let’s see something more interactive – with LiveView’s phx-keypress, phx-keyup, and phx-keydown events, even LiveView powered games are a viable option. For example, here’s a feature-complete snake game, in 330 LOC, which requires zero user-land JavaScript. We can see it action here:

Client-Side Applications Aren’t Going Away

As much as LiveView shines for the types of use cases we’ve covered, there will always be a place for code running on the client. Certain use-cases and experiences demand zero-latency, as well as offline capabilities. This is where JavaScript frameworks like React, Ember, etc., shine. For example, our JavaScript engineers at DockYard are working with fortune 500 companies with offline-capable requirements since they have employees out in the field with limited connectivity. This adds complexity, but it’s easily justified given the clear business requirements. Fortunately, we have great client framework options when it comes to these kinds of experiences. If you’re building desktop-like features, the next Google Docs competitor, or an offline-capable web application, JavaScript frameworks are there to enable your needs. Otherwise, if you need bits of rich interaction, such as real-time validations or autocomplete, multi-step forms, or even simple games, LiveView will be a fantastic choice. It will also offer savings in payload sizes compared to JavaScript framework alternatives:

NameSize (minified)
LiveView.js + morphdom29K
Vue 2.5.2088K
React 16.6.3 + React DOM112K
Ember 3.0.0.beta.2468K

Not only is LiveView.js + morphdom much lighter than the JS frameworks, the frameworks are just the baseline. You still need to ship application-specific JS and often add supporting JS libraries such as react-router, redux and friends to get feature parity.

What’s Next?

We’re close to an initial release, and we can’t wait to see the kinds of applications the community builds with LiveView. Our next step is building testing tools similar to Phoenix.ChannelTest,which will allow testing live views and client interaction purely in Elixir without a browser. We will also be shipping LiveView generators that will allow bootstrapping CRUD UIs like the existing phx.gen.html generator, but fully real-time out-of-the-box. Stay tuned!

DockYard is a digital product agency offering exceptional user experience, design, full stack engineering, web app development, custom software, Ember, Elixir, and Phoenix services, consulting, and training.