Talking to the LiveView

Updating the Phoenix LiveViews from outside the process

LiveView is the new cool thing in the Phoenix world. I will not try to say what it does and how it works. There are already many articles and tutorials doing it. I will skip also the installation part, which is covered by the docs.

So let’s start directly with our question. How other application processes can communicate and update the LiveViews?

The Example

This time I’ve chosen a real example, a personal project I’ve been working on. It’s a basic Phoenix app, with server rendered templates (EEX) and very little JS.

The challenge is simple. Update the number of recommendations after the user chooses a favorite product.

Of course, this can be achieved in other ways, for example with “normal” Phoenix channels. But such a small task would be perfect to get myself introduced to the LiveView.

The Synchronous Way

Before we start. When I say synchronous I do not mean the handle_event/3 callback of the LiveView (which is not anyway). But rather the logic that happens inside this function, before the user can see the update view.

Let’s explore first what I consider to be the easiest approach. Then we will see why I didn’t choose this one.

The add to favorites button HTML can be something like:

<button phx-click=”add_favorite” phx-value=<%= [product.id] %>> 🖤</button>

And then the LiveView:

On mount, the LiveView retrieves the user, products and current recommendation count and adds them to the socket assigns.

The user clicks the favorite icon 🖤. This sends an add_favorite event to the LiveView. The event handler updates the user recommendations based on their new favorite products. Then gets the new number of recommended products and puts it in the socket assigns. This triggers an update of the LiveView and voilà! The recommended products count is live updated.

The Problem

It works. But it may be slow. update_recommendations/1 does a lot of queries and other time-consuming stuff. I would not want to link together the two separate actions happening here:

  1. the user adds a favorite product — should be as fast as possible

2. the user sees an updated number of recommendations — can be delayed

If we do so, the response to the first point will be delayed. The user may be tempted to click 🖤 again, triggering a remove from favorite. Of course, there are other ways to avoid this, but that’s not our concern right now.

The “Semi-Synchronous” Way

We want to separate the 1 and 2 points above. It proves just a matter of splitting the handle_event/3 function.

The handle_event/3 sends a message to the LiveView process (self()). Then updates the user interface with the new favorite product. The message gets then picked up by handle_info/2, who will process and update the recommendations.

The Problem

While it solves the issue above, it is still not very efficient. Imagine the user adding a favorite, and immediately adding another one. For the first favorite will receive rapid feedback. But the LiveView process is blocked until the update_recommandations/1 will finish for the first request. Only then the user will receive an update for their second favorite product.

That’s why I called it a “Semi-Synchronous” way 😄.

The Asynchronous Way

In reality, the above solutions will not even work for my app. The update_recommendations/1 is already handled asynchronously. I would get back the count before the new recommendations are even computed.

So we need a way for the Recommendations server to communicate back to the LiveView process when it finishes its job.

The first attempt — pass the PID as an argument

That’s the easiest approach. When casting to the Recommendations server, pass the LiveView pid as argument. For example: QuickPick.Products.update_recommendations(user, [live_view_pid: self()]).

After the server updates the recommendations, it should call back the LiveView like: send(live_view_pid, :update_recommendations).

Straightforward, but I decided to keep looking for other ways, for one reason. It cannot handle updates that are not triggered by the user. For example, a callback from an external provider could trigger a products refresh or so. The callback will not have access to the LiveView pid, and cannot send it a message.

The second attempt — use the Registry

I registered the LiveView processes on mount. Then the Recommendations server would search the registry and send a message to the process. However, I soon realized this may not be a sustainable solution if the application is deployed on multiple servers. In my previous article I discussed exactly this topic:

So I will not insist on this approach.

Third attempt — PubSub

Luckily, checking the examples provided with the docs, I saw Chris McCord using Phoenix.PubSub for communicating with the LiveViews.

I created a module responsible for the communication between the LiveViews and the rest of the app.

It creates either a generic or a user-specific PubSub subscription. Then enables notifications for both cases.

The message in the notification follows the following pattern: {calling_module, event, result}

  • the calling_module is the module that requests the live update. Mainly for statistics and debugging purposes. But can be used later on for pattern matching as well.
  • event is a list of atoms describing the triggering event. Used for pattern matching in the LiveView. Eg. [:recommendations, :updated].
  • result the data that is passed back to the LiveView.

The Recommendation server notifies the LiveView when the update is done:

LiveUpdates.notify_live_view(
user.id,
{__MODULE__, [:recommendations, :updated], []}
)

The LiveView should just PubSub subscribe on mount and handle the messages:

I find this a flexible solution that permits not only external processes to communicate with the LiveView. But also communication between different LiveViews, or mass updates for all users or group of users. But maybe we can explore those options in a future article.

Now your turn. I would be very curious to see your opinions and what would be your choice for communicating with the LiveViews.