Skip to content

LiveView: Soft real-time interactive application without using Javascript

cover

LiveView is an Elixir/Phoenix library. It enables us to implement interactive applications without using javascript beyond the bare minimum. In this post, I will try to illustrate how it works, by showing a use case where I successfully applied it.

I propose the following (common case) scenario: We need to filter data from a database and the size of the data is big enough to discourage from filtering directly in javascript.

There are 2 approaches that quickly come to mind (which I assume the reader is familiar with):

  1. Form submitted via http, with the fields being the parameters to filter
  2. Javascript application using ajax to retrieve the information from an API

The first approach requires a page reload and will not be interactive at all. The second approach does not need to reload the page and can be made to be highly interactive, but additional complexity is added as a separate application needs to be created and maintained for this seemingly simple task.

LiveView provides a third way: Writing server-side code only, while providing real-time interactivity and avoiding page reloads. You can find detailed information at https://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript

The approach is basically as follows:

  1. The user sends a regular HTTP request to the page where LiveView is being used
  2. The view is mounted (an actual function named mount is called, initializing the view)
  3. The html is rendered and sent to the client
  4. The client connects to LiveView via websockets
  5. A stateful connection is established

Once the connection is established, there are some events in the client that propagate to the server, the server updates the state, and a minimal DOM diff (DOM differences with respect to the old version) is then sent to the client to be rendered.

I will focus on implementing the proposed scenario. For installation instructions, please read the documentation. Also, we will use a function named retrieve_items which optionally receives a map with the filters to apply. This function won’t be implemented in the following exercise, we will just assume that it correctly retrieves the information from the database and filters as expected.

There are 4 things that need to be done to implement our LiveView:

  1. Decide where it will be served from
  2. Add a mount function
  3. Modify the template to send the client events
  4. Listen for client events

I will go through every point in this list in detail.

Decide where it will be served from

LiveViews can be attached to routes, controllers or templates. We will go with templates this time:

def index(conn, _params) do
  live_render(conn, MyApp.MyLiveView)
end

Add a mount function

The mount function is where we initialize the LiveView. We basically establish what a “stable known state” is. In our case, we can simply add a blank filter (unfiltered data):

def mount(_params, socket) do
    my_items = retrieve_items()

    {:ok,
     assign(socket, %{
       items: my_items,
       filter_state: %{
         "param_a" => "",
         "param_b" => ""
       }
     })}
end

Modify the template to send the client events

Add the events to the template so we can react to changes:

<%= form_for :my_filter, "#", [phx_change: :filter, phx_submit: :ignore], fn f -> %>
    <%= label f, :param_a, "Filter by param_a" %>
    <%= text_input f, :param_a, class: "form-control", "Search by param_a", value: @filter_state["param_a"] %>
    <%= label f, :param_b, "Filter by param_b" %>
    <%= text_input f, :param_b, class: "form-control", "Search by param_b", value: @filter_state["param_b"] %>
<% end %>

<%= for item <- @items do %>
  <%= item.name %>
<% end %>

Listen for client events

Add the functions to react to client events:

  def handle_event(
        "filter",
        %{"_target" => ["my_filter", param], "my_filter" => my_filter},
        socket
      ) do
    %{^param => value} = my_filter
    filter = Map.put(socket.assigns.filter_state, param, value)
    my_items = retrieve_items(filter)

    {:noreply,
      assign(socket,
      items: my_items,
      filter_state: filter
    )}
  end

And, we are done. The form has `phx_change: filter` when declared. These values tell LiveView to call the handle_event method with the `filter` parameter. You could have several of those parameters and react as necessary in those cases.

Final words

LiveView is an approach that is gaining traction. There are other libraries that are trying to do something similar in other frameworks (like, for example, Reactor for Django). I found LiveView very easy to work with, and the sensation you get when you are on the user side is that it is fast. Obviously, this is a very simple example, and more sophisticated things can be implemented, so give it a try.

There are a couple challenges to face when working with this approach:

  • This is a non-traditional approach, so it is probable that you are not familiar with it and you will have to read a lot about the underlying technologies to be able to grasp a good understanding of the implementation.
  • You are not working on the client side anymore. There will be no Javascript by default. There are ways to integrate it with some client side technologies, but that will probably take more time than doing it directly with JS and ajax. There are many other benefits that LiveView broughts to the table, but in this case it is worth evaluating what is your requirement before proceeding with it.

So far, I am satisfied with what I have been able to achieve, and I intend to keep using it in the future.