Need help linking a phoenix LiveView to a phoenix channel

I’m relatively new to phoenix, so I may not have the vocabulary to express myself clearly.
I ended up maintaining as a junior a phoenix application with different channels that each have subtopics.
Some new requirements came in and I opted to do that inside of a LiveView.
I would like to receive updates from a specific channel inside the LiveView, change the assigns and update some content that way.

defmodule MyApp.RoleBillboardLive do
  use Phoenix.LiveView
  import MyApp.{UserSocket, Endpoint}
  alias MyApp.Endpoint
  alias Phoenix.Socket.Broadcast
  def render(assigns) do
    ~H"""
    Message from server: <%= @param %>
    """
  end

  def mount(_params, _, socket) do
    topic = "global:test"
    MyApp.Endpoint.subscribe(topic)
    IO.puts("I am now subscribed to " <> topic)
    param = "This is a test,"
    {:ok, assign(socket, :param, param)}
  end

  #def handle_info(_, socket) do
  #  IO.puts("info")
  #  {:noreply, assign(socket, assign(socket, :yeet, "yote"))}
  #end

  @impl true
  def handle_info(msg, socket) do
    IO.inspect(msg)
    {:noreply, assign(socket, :param, msg)}
  end
end

When I run this code, the mount function is called, I can see it clearly in the terminal.
(I run the command iex -S mix phx.server)
The page loads as expected with the initial content,
but when i publish a message on the topic, handle_info is never even called

I tried to skim through the docs, and a few forum posts, but none of them seemed to be advising on what I’m trying to do.

1 Like

Hey @perceval62 welcome!

Can you show the code you are using to publish? Also to confirm, you’re doing the publish call in the same iex session that is running the server, not a separate one right?

Finally in here:

  @impl true
  def handle_info(msg, socket) do
    IO.inspect(msg)
    {:noreply, assign(socket, :param, msg)}
  end

You should change that to assign(socket, :param, inspect(msg)) because otherwise if you get a message that isn’t a string it’s going to crash when it tries to render it.

1 Like

Thx for the quick response @benwilson512 !

Yes I am publishing from the same IEX session that the phx server is running on.
It is a big code base, so I don’t know exactly where all the stuff is broadcasted from.
Throughout the code with some ctrl-f ing, I can see that most broadcast calls are made with this function

def broadcast_global(event_name, payload) do
    MyApp.Endpoint.broadcast("global:test", event_name, payload)
  end

it’s also pretty much equivalent to what I tried doing in the IEX session

If you are doing MyApp.Endpoint.subscribe("global:test") and then you MyApp.Endpoint.broadcast("global:test", "hello", "world") that should work just fine. I see you had commented out another handle_info clause, is that actually commented out or not?

If you run in your iex session (here using my app’s name Sensetra)

iex(1)> Sensetra.Endpoint.subscribe("test")
:ok
iex(2)> Sensetra.Endpoint.broadcast("test", "hello", "world")
:ok
iex(3)> flush()
%Phoenix.Socket.Broadcast{topic: "test", event: "hello", payload: "world"}

you should see your hello world there at least .

1 Like

@benwilson512
Yeah, it’s actually commented out. Its just me trying out different combinations of stuff I googled :sweat_smile:

Anyways, I did exactly what you did in the iex code snippet, and it works .
So now I just need to figure out why my handle_info in my live_view is never called …

iex(4)> MyApp.Endpoint.subscribe("global:test")       
:ok        
iex(5)> MyApp.Endpoint.broadcast("global:test", "hello", "world")
:ok
iex(6)> flush()                                           %Phoenix.Socket.Broadcast{
  event: "hello",
  payload: "world",
  topic: "global:test"
}

I guess i didn’t flush in my console before. Is there some manipulation I should do like flush when dealing with code instead of the REPL ?

no.
the flush is a way to empty the process inbox, it’s a helper from the repl, you don’t need that in normal code, on normal code you should use receive

receive do
  message -> do_something(message)
end

a phoenix channel and live view are genservers, that is an abstraction of a infinite loop of receive blocks.
a phoenix channel usually handles all %Phoenix.Socket.Broadcast{} and send it directly to the websocket, if you want to do something with it before sending to the ws you need to intercept the particular event.
a live view has a callback, iirc it’s handle_event to deal with incoming events before dealing with it in the live view.

all that is a oversimplification of the thing. tthere are somewhat more details in the docs.

edit:
handle_event is for receiving client events, i’m not sure if the right way to deal with pubsub messages in live views would require to intercept it just like in the channel approach.

You might have to check for connection

if connected?(socket) do
  ...
end
2 Likes

That is a great point. @perceval62 you really should do this in your mount/3

  def mount(_params, _, socket) do
    if connected?(socket) do
      topic = "global:test"
      MyApp.Endpoint.subscribe(topic)
      IO.puts("I am now subscribed to " <> topic)
    end

    param = "This is a test,"
    {:ok, assign(socket, :param, param)}
  end

When you first connect to a liveview app you get two calls to mount, one for the static render and one for the live render. I was actually just talking about this in another thread: More convenience functions to deal with assigns inside LiveView - #15 by benwilson512

2 Likes

Oh so does that mean that my liveview only subscribes once with the static viewer but then the process gets cleaned once its rendering live ?

I don’t have access to my good computer setup right now but I’ll definitely implement those and see what happens.

I did some more testing before leaving work, but the code worked as expected when testing

The code as you have it now will subscribe twice in roughly the following sequence:

  1. Static mount runs
  2. You call subscribe, which subscribes the HTTP handling process
  3. HTTP request finishes, so the HTTP process terminates
  4. That subscription is cleaned up automatically
  5. live mount runs
  6. Subscribe calls again, this time subscribing the websocket process
  7. mount completes, but the process remains alive and subscribed.

SO even though it’s happening twice (which is unnecessary, my code removes step 2) it should still work overall. What I and I think others are wondering is if the live mount is happening at all. Maybe in your effort to make a minimal example you accidentally stripped off the javascript and so no liveview is actually happening?

That might be the case, but then I should at least see outputs from IO.puts in my handle_info function when it gets called when a broadcast happens

Assuming the client never calls back and the live/connected mount doesn’t run, handle_info will never be called as the only process that subscribed during the static mount gets terminated.

I’d second @benwilson512’s suggestion to check if that’s what’s happening.

@perceval62 no if you only see one "I am not subscribed to" message (in your code as you provided it) then you will never get a handle_info. The static render does not keep a persistent process around that is available to receive messages. You must have the javascript on the front end initiate a websocket connection so that the persistent websocket process is available in the server to receive messages (like a broadcast).

Okay, I get my behaviour working by implementing my code in the frontend JS code.

Just so I understand, would there be a way to implement such pubsub behaviour only in the view ?

a bit like I was trying to do: retrigger a display when a pubsub message arrives ?

Are you saying you accomplished the UI update with just the javascript code or did you get LiveView working? Can you show the code you used?

What you wrote should have worked just fine. We could help you better if you answered some of the earlier questions regarding whether your IO.puts lines print once or twice.

I accomplished the UI with just the javascript code,
I didn’t get the LiveView to work and update from just the backend code.

Sorry for the delays, I had some issues setting up my home workstation to be like the one at my office …

Here’s the JS frontend code:

socket.connect()

// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("test", {})
channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })


const first = "https://myAppCdn.com/something.png"
const second = "https://myAppCdn.com/something_else.png"

channel.on("first", (payload) => {
  const image_div = document.getElementById("image_container")
  image_div.src = first
})

channel.on("second", (payload) => {
  const image_div = document.getElementById("image_container")
  image_div.src = second
})

export default socket

here’s the backend code now:

defmodule MyApp.RoleBillboardLive do
  use Phoenix.LiveView

  @impl true
  def render(assigns) do
    ~H"""
    <script src="/js/app.js"></script>
    <img id="image_container" src="https://myAppCdn.com/something.png" />
    """
  end

  @impl true
  def mount(_params, _, socket) do
    param = "This is a test,"
    if connected?(socket) do
      IO.puts("subscribed to topic")
      MyApp.Endpoint.subscribe("global:test")
    end
    {:ok, assign(socket, :param, param)}
  end

  @impl true
  def handle_info(msg, socket) do
    IO.puts("YEEEEEET")
    param = "A very very different message"
    {:noreply, assign(socket, :param, param)}
  end
end

As for the IO.puts, I did see the messages only once.
But I stopped seeing “subscribed to topic” once I put a connect? check.
And, of course I never saw a “YEEEEEEET”

Right so this means that your browser is not connecting to live view at all. Can you show the javascript code you have to connecto to liveview?

I’m not sure if I understand isn,t that the code I sent ?
Is there some docs i missed?

I’m not sure I understand what you mean by show the js code, Isnt that what i just sent ?

Did I miss something in the docs ?

I just read this Phoenix.LiveView — Phoenix LiveView v0.20.0

I guess im missing the live render tag and the live socket

Woops I feel really goofy now :sweat_smile: