Swapping React for Phoenix LiveView

I‘m not saying this is a good idea, but it’s fun to try

How LiveView might replace existing components of our system

Recently there has been some buzz in the Elixir community surrounding Phoenix LiveView, which promises SPA-like experiences without the need to write and maintain a heavy JavasScript application.

This is a pretty exciting premise, but I‘ve been curious how things might work out for a “real” use-case. In this article I’ll explore a rough reimplementation of part of our production app, but using LiveView instead of React.

A (slightly) polished, deployed version of this demo can be found here.

The existing application

Here at Qixxit, we use Elixir for our backend application. Given travel dates, an origin and a destination, we plan a range of route options by combining modes of transport from various sources, then allow customers to purchase all the tickets needed for their journey in a single transaction.

The real Qixxit in action

Certain characteristics of our backend application made Elixir a great choice. For instance, we talk with various 3rd-parties, but we have no need to wait for any given provider before communicating with the next, meaning much of the work done can be performed asynchronously. Once we have some data to send a bookable thing to a client, we can send it right away instead of waiting for the entire list of results, so Phoenix Channels are a great fit.

On the web front end, we have a React application. Results are rendered in real-ish time after a user performs a search.

In order to get this working without React, we’ll need a couple of key features:

  • Autocompletion of origin/destination
  • Rendering of search results

Now let’s look at how these might be implemented with LiveView.

Autocompletion of origin/destination

First up, we’ll allow users to tell us their origin and destination. We have an existing service which returns a list of places when passed a string which looks something like a place name, so we just need to hook into this from LiveView.

This can look something very close to the autocomplete implementation in the example repo. For us, the key difference is that we need 2 autocompleting fields instead of 1. As LiveView’s handlers work on the form as a whole, rather than its inputs, we need to identify which field to send suggestions to as our user types. Here’s a possible implementation:

This works like so: A user starts typing in the origin field, with each keypress triggering the "suggest" event on the server. Initially, we have nothing set for socket.assigns[:origin_query], so we enter the 1st cond branch, where we return potential matches under socket.assigns[:origin_matches].

These matches are then rendered on the client. Now when the user selects the item they are interested in, the input’s value is set to the matching result. When they click away from the element, we trigger "set_origin", which assigns this value to the socket’s :origin_query.

Next, they start typing in the destination field. We trigger "suggest" again, but now query["origin"]matches socket.assigns[:origin_query], so we do not enter the 1st branch again. Instead, we perform the same steps as above, but for the destination instead.

Look Ma, no JS!

With this working, we can look at constructing and rendering results.

Rendering of search results

Now that we have a way for users to find places they want to travel between, we need to construct itineraries which allow them to make the journey, then render them. The change looks like so:

First, we add a date input (users need to tell us when they would like to travel). Now when the form is submitted, we trigger "search", which spins up our existing Query service. This coordinates routing, contacting providers, and constructing bookable things. We tell this process to send results back to us by pid.

When we receive results in :new_results, we simply assign them to the socket. LiveView then handles rendering these in the client.

Rendering results as they come in

I’ve put a little time into styling this up some, and deployed a version here, so please try it out!

Conclusion

Overall, I was impressed by how straight forward this was. With only a few hours work I was able to get a rough working equivalent of a fairly complex part of our client-side application, which I think is pretty powerful.

A few areas I’d like to explore further:

  • Figuring out how to manage the inputs in isolation while watching the form as a whole. While I came up with a solution, I’m not sure it’s ideal;
  • Sorting the results. Performing this work whenever a new result comes in means I always need to send the entire result set over the wire. Perhaps this could be improved;
  • Deciding how to send results from Query to the LiveView. I opted for a simple Process.send/3, but more thought is needed around what is the best approach;
  • How does this behave under load? We’ve moved work from clients to the server, would we notice performance issues? The autocomplete already seems sluggish at times, what’s causing this?

I’m excited to see what others can do with LiveView as it approaches 1.0! If you’re interested in more detail on how LiveView works, Chris’ keynote from the recent ElixirConf EU is worth a watch.