Fundamentals of passing data from the Plug connection to the LiveView

After a lot of reading and some experimentation I was able to pass my “locale” value from the Plug connection to my Liveview. The problem is that I though I was understanding how everything worked, but realised I’m not yet there as when trying different approaches I couldn’t and I still have an issue I don’t understand in my actual working code.
I believe it would be very helpful to everyone like me to clearly understand this picture. So I would first present my current approach that is working, point one issue and present alternative ways I though should work but don’t in hope someone could clarify the details.
As context, I have a Module Plug that properly injects a :locale value in the Conn assign(conn, :locale, "value").
A) Current working approach:
Router: live "/privpol", Priv_polLive, session: %{"locale" => @locale}
Liveview:

defmodule MyappWeb.Priv_polLive do
  use MyappWeb, :live_view

  @impl true
  def mount(_params, %{"locale" => loc}, socket) do
    IO.inspect loc
    socket = assign_new(socket, :locale, fn -> loc end)
    IO.inspect socket.assigns.locale
    {:ok, socket }
  end
end

Questions:

  1. In my console I can see a nil and a locale value on the same rendering. Why?
[debug] Processing with Phoenix.LiveView.Plug.Elixir.LiveappWeb.Priv_polLive/2
Parameters: %{}
Pipelines: [:browser]
nil
"en"
[info] Sent 200 in 2ms
  1. Visual Studio has a warning in my Router code saying: undefined module attribute @locale... Makes sense as @locale should only work in views and templates. but then why is it working?

B) Alternative 1: trying to pass the locale to the session in my Plug using put_session(conn, key, value). Seems very easy and as long as locale is in the session I was expecting it would be available in my 2nd mount in the Liveview inside socket. I tried to change the code in there and in my plug but couldn’t make this work. Maybe I’m missing some other configuration…

C) Alternative 2: trying to use the new get_connect_info(socket) in the Liveview mount, after reading the docs and adding my locale to the connect_info: in my endpoint:

socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [:locale, session: @session_options]]
This brings an error at the endpoint side.

So, are all these 3 alternatives really alternatives or is there only 1 or 2 approaches that would work? Why?
What about the issues in my current working solution and how to tie it to the life cycle, mounting twice, specially the console nil followed by the "en"? What’s the proper way of passing the locale in the router to avoid warnings?
Thank you in advance.

3 Likes

There are two ways:

  1. By storing it in the session. You can define a plug that executes in your router and calls put_session to store the locale in the session. Then it will be available in the session in mount/3. This is the preferred approach and we even have an example with it in the docs: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#module-using-gettext-for-internationalization

  2. Via get_connect_params. Open up your app.js and you will see there are some parameters passed to LiveSocket. If you wrap the locale on the page, in a meta tag for example, you can pass it down. This is how we handle things like csrf_token. You will be able to read it in your LiveView by calling Phoenix.LiveView.get_connect_params. Note though it will only be available in the websocket connection

15 Likes

Clear, thank you!

Well…assign_new() is still a mystery how it exactly works.
@josevalim was sharp in the 2 recommended alternatives and details. Clear!
But as my example with assign_new() shouldn’t work and was working I kept investigating and discovered that as long as I pass in the router , session: %{"locale" => "some_value"} the call to assign_new() in my Liveview would always work properly, regardless of the value I was passing in the router.
So I tried to push it a little bit further and using plugs injected 2 values in Conn in my pipeline:

  • assign(conn, :test, "Nooo")
  • assign(conn, :locale, "en")
    Then, in the router injected in the session:
    live "/privpol", Priv_polLive, session: %{"locale" => "lol", "test" => "UaU!"}
    Notice the difference in the keys: :atoms in the conn, Strings in the session.
    Finally in my Liveview I have:
def mount(_params, %{"locale" => loc, "test" => test}, socket) do
    socket = assign_new(socket, :locale, fn -> loc end)
    socket = assign_new(socket, :test, fn -> test end)
    IO.inspect loc
    IO.inspect test
    IO.inspect socket.assigns.locale
    IO.inspect socket.assigns.test
    {:ok, socket}
  end

Interestingly everything works and I get the results I expected from conn, not session (I get this in my console):

[debug] Processing with Phoenix.LiveView.Plug.Elixir.LiveappWeb.Priv_polLive/2
Parameters: %{}
Pipelines: [:browser]
"lol"
"UaU!"
"en"
"Nooo"
[info] Sent 200 in 2ms

What puzzles me is that I need to pattern match on the session in my mount/3 in the Liveview to make it work, although the values that assign_new() access as variable are different than the values of the variable!!!
And I’m pattern matching session using string keys and getting the value of atom keys in the conn!!!

Again, …lost! :slight_smile:

In your logs, you are showing the results of the initial render (based on conn). assigns_new basically tells you to use the existing conn.assigns for that value if one exists. Given your conn has an :locale assign which is not the same as in the session, then they differ.

But note that conn.assigns != conn.session. And yes, one has atom keys, as it is your data, the other has string keys, as it is external data.

2 Likes

Oh I think I understand:

  1. assign_new() will always use the value existing in conn in the initial render if it exists…
  2. …to apply the anonymous function and set the result in assign/3 on the second render.
    If this is correct, then it’s clear, but also means that I can use it:
  • without passing any values to the session in the router as they are not used;
  • can “trick it” like this:
defmodule LiveappWeb.Priv_polLive do
  use LiveappWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign_new(socket, :locale, fn -> "lol" end)}
  end

end

as I need to have an anonymous function in assign_new(), but if I only want to access the value in Conn on the first render and pass it into socket on the second… :slight_smile:

Not a best practice, I understand but I think I finally understand how it works.
Thank you very much @josevalim , hope it is correct and could be useful for others as well.

This won’t work as the assigns are not available when the LiveView is mounted on the websocket. assign_new is an optimization mechanism, as describe in its docs, it is not a replacement for what you want to achieve. I would not use in the case above - it has no benefits.

1 Like