How to get app.html.heex to use assigns properly with LiveView

Background

I have a Phoenix application, where all pages (expect the login page) have a menu at the top.
This menu will therefore only appear if the user has already logged in.

I am trying to replicate this behaviour, by incorporating said menu in the app.html.heex so I don’t have to repeat it constantly.

However, no matter what I do, the menu is never displayed.

Code

I am trying to change my app.html and verify if the user logged in using assigns[:user].

app.html.heex:

<header>

 <%= if assigns[:user] do %>
   <h1> Cool Menu here </h1>
 <% end %>

</header>

<main class="">
  <div class="mx-auto max-w-2xl">
    <.flash_group flash={@flash} />
    <%= @inner_content %>
  </div>
</main>

The login process is async, as shown here. Basically I send a login request to a Manager, and when it feels like replying back, I update the socket with assigns[:user] and redirect to a cool page.

user_login_live.ex:

defmodule WebInterface.MyAppLive do
  use WebInterface, :live_view

  @impl true
  def mount(_params, _session, socket), do: {:ok, socket}

  @impl true
  def handle_event("login", params, socket) do
    IO.puts("Seding async request")
    :ok = Manager.login(params)
    {:noreply, socket}
  end

  @impl true
  def handle_info({:login,  user, :done}, socket) do
    IO.puts("Authentication succeeded for user #{inspect(user)}")

    updated_socket =
      socket
      |> assign(:user, user)
      |> redirect(to: ~p"/cool_page")

    {:noreply, updated_socket}
  end

end

I would expect the cool page to have the <h1> menu, but that is not the case.

Questions

  • What am I doing wrong here?
  • Isn’t the assigns updated automatically?
  • Does app.html.heex work differently from other files?

Shouldn’t the assigns be referenced as @assigns?

Given this code does work:

I believe I dont need the @ symbol.

For reasons beyond my understanding, I can confirm that assigns[:user] is not being updated automatically. As a consequence, the if statement always evaluates tofalse.

I don’t know if this is happening because of a race condition, or if handle_info calls are simply not supposed to update the socket.

Don’t use assigns[…] anywhere with LV. Always use @user and assign default for things, which might not always be available.

1 Like

But here lies the problem. When the client first sees the page, @user does not yet exist, therefore if I run if @user the code will crash with a 500.

I could set a default for user, like you suggested. However, even after trying that, @user is still not updated after def handle_info({:login, user, :done}, socket) do finishes running and @user remains with the default value.

I have failed to understand why this is happening …

On a different note:

Don’t use assigns[…] anywhere with LV.

I got this from an Elixir course I did. Could you elaborate on why you believe this is a bad practice?

directly use @variables.I think assigns does not manually exists like this @var

assigns is not available as variables. give default values during mount . generally whatever value we want to use , are initialised to default value or nil during mount.

I need you to elaborate on that. If assigns[:user] exists, then I can either use assigns[:user] in the code or @user. They appear to do the same, differences being the first form is more explicit while the second more convenient.

It is also worth mentioning that assigns is not the same as @assigns. To have an @assigns in your code would be equivalent to assigns[:assigns], iirc.

Generally speaking, avoid accessing variables inside LiveViews, as code that access variables is always executed on every render. This also applies to the assigns variable.

https://hexdocs.pm/phoenix_live_view/assigns-eex.html#pitfalls

2 Likes

Yeah in the modern era you end up needing to pre assign(:current_user, nil) somewhere. You can enforce this at compile time by using a functional component for your header:

attr :current_user, :map, required: true
def header(assigns) do
  ~H""
end
3 Likes

Following @LostKobrakai 's advice I have updated the code to:

app.html.heex:

<header>

 <%= if not Enum.empty?(@user) do %>
   <h1> Cool Menu here for user <%= user.name %> </h1>
 <% end %>

</header>

# other code

user_login_live.ex:

defmodule WebInterface.MyAppLive do
  use WebInterface, :live_view

  @impl true
  def mount(_params, _session, socket), do: {:ok, assign(socket, :user, %{})}

  @impl true
  def handle_event("login", params, socket) do
    IO.puts("Seding async request")
    :ok = Manager.login(params)
    {:noreply, socket}
  end

  @impl true
  def handle_info({:login,  user, :done}, socket) do
    IO.puts("Authentication succeeded for user #{inspect(user)}")

    updated_socket =
      socket
      |> assign(:user, user)
      |> redirect(to: ~p"/cool_page")

    {:noreply, updated_socket}
  end

end

Which now errors with:

** (exit) an exception was raised:
    ** (KeyError) key :user not found in: %{
  __changed__: %{inner_content: true},
  flash: %{},
  inner_content: %Phoenix.LiveView.Rendered{
    static: ["<!--\r\n  This example requires updating your template:\r\n\r\n  ```\r\n  <html class=\"h-full bg-gray-100\">\r\n  <body class=\"h-full\">\r\n  ```\r\n-->\r\n<div class=\"min-h-full\">\r\n \r\n\r\n  <header class=\"bg-white shadow\">\r\n    <div class=\"mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8\">\r\n      <h1 class=\"text-3xl font-bold tracking-tight text-gray-900\">Activate</h1>\r\n    </div>\r\n  </header>\r\n  <main>\r\n    <div class=\"mx-auto max-w-7xl py-6 sm:px-6 lg:px-8\">\r\n      <!-- Your content -->\r\n    </div>\r\n  </main>\r\n</div>"],
    dynamic: #Function<0.92593063/1 in WebInterface.ActivateLive.render/1>,
    fingerprint: 220397200052326388454228465496201353196,
    root: false,
    caller: :not_available
  },
  live_action: nil,
  socket: #Phoenix.LiveView.Socket<
    id: "phx-F3kV873WgkDirAQB",
    endpoint: WebInterface.Endpoint,
    view: WebInterface.ActivateLive,
    parent_pid: nil,
    root_pid: nil,
    router: WebInterface.Router,
    assigns: #Phoenix.LiveView.Socket.AssignsNotInSocket<>,
    transport_pid: nil,
    ...
  >
}

I don’t quite understand how it is complaining about the key when a default value is set.

Without a stacktrace it is hard to help debug that error.

Tangentially I think a default of nil is a lot more intuitive than %{}.

2 Likes

Seems to me everyone is focussing on the wrong thing… The assign(:user, user) here won’t do much since you immediately redirect, which reloads the page and lose the state.

Do you somehow expect the user assign to be kept? Maybe you want an on_mount lifecycle hook to set the user assign.

2 Likes

Yes!

To this effect, would push_navigate not work here (instead of redirect)?

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_navigate/2

How would I do it?

I’m not sure exactly how your application is doing the authentication, but looking at LiveBeats as a starting point should get you going.

In your router, you can define an on_mount hook.
See doc here or here.

See the examples of on_mount hooks in user_auth.ex.

I don’t think this process is applicable to my use case.

I am simply making a request to an API, and then saving the parsed answer. It is so simple, I think calling it authentication is a stretch.

Perhaps a cookie would make more sense here?

“Authentication” just means “login a user”, it’s certainly not a misnomer no matter how simple it is!

I’m assuming you’re getting this error because /cool_page is a different LiveView (which I’m gathering from view: WebInterface.ActivateLive). You have set the default user in the mount of WebInterface.MyAppLive which then redirects to /cool_page which I’m assuming is WebInterface.ActivateLive. Since this is a totally different LiveView, it’s a totally new socket (as @champeric pointed out).

Yes, if you are logging in a user you have to store a successful login id in a session. The thing is, you can’t set session data over a websocket. This is a websocket limitation, not a LiveView one. You will have to create a controller that looks something like this:

defmodule WebInterface.LoginController do
  use Phoenix.Controller

  def login(conn, params) do
    user = Manage.login(params)
    
    conn
    |> put_session(:user_id, user.id)
    |> redirect(to: ~p"/cool_page")
  end
end

This is where on_mount comes in. It’ll look very roughly like this:

def on_mount(:admin, _params, %{"user_id" => user_id}, socket) do
  {:ok, user} = Manage.get_logged_in_user(user_id)

  {:cont, Phoenix.Component.assign_new(socket, :user, user)}
end

These docs are well worth reading—they go into a lot more detail.

Correct !

Given this is for a Desktop application, wouldn’t it make more sense to perhaps use an Agent or ETS tables for this purpose?

Would it be any major differences VS having to create a controler and doing it that way?

I also read about not accessing assigns directly in a template. But I always wondered why that seems to be OK to do in the root template of a newly generated phoenix app?

e.g.