Phoenix LiveView: Pushing events *from server* to client

With release of hooks we got an ability to push events from client to server via pushEvent(event, payload). But is there a way to do the opposite - push an event from a LiveView module on server to a hook on client?

An ideal implementation looks like this in my mind. The server part:

defmodule MyAppWeb.SomeLive do
  # ...
  def handle_event("whatever", _params, socket) do
    # Here I do something with the socket to push an event
    message = "custom-message"
    payload = %{foo: "bar"}
    push_custom_message(socket, message, payload)

    # And return socket as is, without assigning any changes
    {:noreply, socket}
  end
end

The client part:

// ...

export const liveSocket = new LiveSocket("/live", Socket, {
  hooks: {
    SomeHook: {
      // And here I receive all custom messages sent from server via `push_custom_message/3`
      messageReceived(message, payload) {
        console.log(message); // "custom-message"
        console.log(payload); // {foo: "bar"}
      }
    },
  }
})

// ...

The point is that some parts of UI are tightly controlled by JS, and there is no way I can render them on the server, and the most convenient way for me is to trigger an event from the client, receive it on the server and sent back some data through WebSocket.

As an example, imagine an infinite scrolling widget fully controlled by JS, with phx-update=ignore, and when I click the “Load More…” button a “load_more” event is sent to LiveView module on the server, which then queries a DB for more data, and sends it back through WebSocket, where it’s received by the hook and rendered in the widget (please note, that this is just an example, the simplest I managed to invent to demonstrate the problem, so please don’t try to solve “how to build an infinite scrolling in LiveView”, that’s not the point).

At the same time I’m pretty happy with all other UI parts to be controlled by LiveVIew, so I don’t want to go all-in with a full SPA. I also don’t want to establish a separate API endpoint (or a socket connection) for this specific widget - because why should I, if there is already a socket established by LiveView? If I was able to send messages through it - it would fit just fine. But I haven’t found a way to.

Also, I’m not experienced neither in Elixir, nor Phoenix, so I might be wrong, but as far as I understood from the code, if I used a regular Phoenix.Channel, I’d be able to do this - to send messages from server to the client. But it seems that in Phoenix.LiveView.Channel the “real” Phoenix.Socket is decorated with Phoenix.LiveView.Socket, and there is no way I can access the former. I also can’t find any suitable functions available externally in Phoenix.LiveView.Channel.

There is also an obvious workaround - I can just render the data I want into the DOM node controlled by the hook, and pick it up on updated, but I believe the socket way would be more correct.

2 Likes

This is what we’ve done to date, and it doesn’t feel all that bad to me. The main reason it feels good is that it preserves the update state => render pattern. You update some state on the server, that drives a new render purely from that state, and then in this case you also embed some new state in the DOM, which drives a new JS render. Adding in message passing creates a whole additional paradigm.

This works great if what you’re doing is trying to show a map or chart or something, although I could see it working less well for other scenarios. I’m not necessarily saying that a direct push feature is a bad idea, just arguing that the “put state in the dom” approach is actually a pretty solid solution.

2 Likes

After thinking for a while I agree that it’s fine - with a bit of abstraction it seems just like another transport. Also, thanks for that pattern part - I missed that, but after you pointed it out, it seems pretty important too.

And I’m not sure, do you confirm with your post that there is no way to make direct push in LiveView? :thinking:

There is not as far as I know.

Okey, thank you!

Also, have you already encountered some pitfalls so far using DOM node as the transport? I can imagine only performance issue, but I don’t expect I’ll get somewhere even close to amounts of data that can cause such issues.

We’ve got charts with ~600k of json data with no observable issue, hope that provides a reasonable frame of reference.

5 Likes

Hey guys, I’m currently asking myself the same questions. It has been a few months since this post, so perhaps there has been something new released.

1 Like

@tovarchristian21

You can send out events from server to client now.

# in `handle_event`
{:noreply, push_event(socket, "points", %{points: new_points})}

# in client
Hooks.Chart = {
  mounted(){
    this.handleEvent("points", ({points}) => MyChartLib.addPoints(points))
  }
}

Read more: https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks

6 Likes

Hi there,

I started by reading this part of the tutorial, but couldn’t get it working, so found myself here hoping someone else might be able to put me on the right tracks.

My LiveView example looks as follows and when I attempt to compile it I get ** (CompileError) lib/zcounter_web/live/counter.ex:8: undefined function push_event/3. I assume I’m doing something wrong with the use importing?

defmodule ZcounterWeb.Counter do
  use Phoenix.LiveView
  def mount(_params, _session, socket) do
    {:ok, assign(socket, :val, 0)}
  end

  def handle_event("yo", _, socket) do
    {:noreply, push_event(socket, "yobacktoyou",  %{points: 100, user: "josé"})}
  end

  def render(assigns) do
    ~L"""
    <button phx-click="yo">-</button>
    """
  end
end

Hi,
are you running the latest version of liveview ?
cheers,

SĂ©bastien.

1 Like

Haha, no I wasn’t! When I checked mix.lock I was using 0.13.3. Updating to 0.14.4 fixed this. Thank-you so much.

What I was a little surprised about wast that this was from a new project generated via mix with the latest Elixir (1.10.4) just a couple of days ago.

It looks like the default mix.exs deps section generated with mix phx.new --live has the following entries:

{:phoenix_live_view, "~> 0.13.0"},
.
.
{:phoenix_live_dashboard, "~> 0.2"},

I had to change these to 0.14 and 0.2.7 respectively for the latest LiveView to be pulled in.

I’m not sure what the best way to help others that might wind up in the same situation as me. It might either be to update the docs to explicitly mention the minimum version of LiveView required, or to update the default deps versions for a new --live Phoenix project.

Thanks again though, I definitely wasn’t looking for this as I was falsely assuming I was on the latest deps by default.

On what phx_new generator are you?

$ mix phx.new -v

To update, run: mix local.phx

1 Like

@sfusato

Excellent question! I hadn’t realised that the generators had versions and their own update lifecycle. Super useful to know, thanks!

Still, it looks like I was on the latest anyway (1.5.4):

C:\Users\samaa\Development\zcounter>mix phx.new -v
Phoenix v1.5.4

C:\Users\samaa\Development\zcounter>mix local.phx
Resolving Hex dependencies...
Dependency resolution completed:
New:
  phx_new 1.5.4
* Getting phx_new (Hex package)
All dependencies are up to date
Compiling 10 files (.ex)
warning: redefining module Mix.Tasks.Local.Phx (current version loaded from c:/Users/samaa/.mix/archives/phx_new-1.5.4/phx_new-1.5.4/ebin/Elixir.Mix.Tasks.Local.Phx.beam)
  lib/mix/tasks/local.phx.ex:1

Generated phx_new app
Generated archive "phx_new-1.5.4.ez" with MIX_ENV=prod
Found existing entry: c:/Users/samaa/.mix/archives/phx_new-1.5.4
Are you sure you want to replace it with "phx_new-1.5.4.ez"? [Yn]
* creating c:/Users/samaa/.mix/archives/phx_new-1.5.4

C:\Users\samaa\Development\zcounter>mix phx.new -v
Phoenix v1.5.4