Add `prepend?` option to `on_mount/1` macro or allow scoped `live_sessions`

Currently, there are two ways to add on_mount hooks to a liveview, you either add them in the live_session macro, or inside each liveview using the on_mount/1 macro.

The order that they will be executed is first the hooks in the live_session call, and then the on_mount/1 calls inside the liveview in order.

Also, live_session calls don’t support having other live_session calls inside them.

This is a bit limiting in some scenarios and make a lot of duplicated code in some complex routers.

Example

In my route, I want to have the following hooks being attached to the liveview on_mount:

/auth” route (contains routes to sign_in and sign_up):

  • Hooks.GetLocale
  • Hooks.GetTimezone
  • {Hooks.Authentication, :not_authenticated}

"/admin"route (contains routes that needs the user to be logged):

  • Hooks.GetLocale
  • Hooks.GetTimezone
  • {Hooks.Authentication, :authenticated}

Notice that both routes require the Hooks.GetLocale and Hooks.GetTimezone on_mount hooks.

So either I need to write two live_session calls that repeats the common hooks in my router:

scope "/auth", Live.Auth do
  live_session :not_authenticated,
    on_mount: [Hooks.GetLocale, Hooks.GetTimezone, {Hooks.Authentication, :not_authenticated}] do
    ...
  end
end

scope "/admin", Live.Admin do
      live_session :authenticated,
        on_mount:  [Hooks.GetLocale, Hooks.GetTimezone, {Hooks.Authentication, :authenticated}] do
    ...
  end
end

Or I can add Hooks.GetLocale and Hooks.GetTimezone to my live_view function (because every liveview in my system should execute these two hooks anyway):

def live_view do
  quote do
    use Phoenix.LiveView,
      layout: ...

    on_mount Hooks.GetLocale
    on_mount Hooks.GetTimezone

    unquote(html_helpers())
  end
end

And then my route becomes simpler:

scope "/auth", Live.Auth do
  live_session :not_authenticated, on_mount: {Hooks.Authentication, :not_authenticated} do
    ...
  end
end

scope "/admin", Live.Admin do
  live_session :authenticated, on_mount: {Hooks.Authentication, :authenticated} do
    ...
  end
end

This second example is better IMO since I don’t need to repeat a long list of on_mount hooks that are common to all my liveviews, but they just works fine as long as no hook in my live_session requires some value first set by these hooks set in the live_view function.

The reason is that the live_session hooks will be executed first and then the ones set in the live_view function. So, for the /auth route, the order would be:

  1. {Hooks.Authentication, :not_authenticated}
  2. Hooks.GetLocale
  3. Hooks.GetTimezone

If Hooks.Authentication requires the locale value, then it will fail because the Hooks.GetLocale is being executed after it.

I see three ways to fix this, the first one is the first example I shown in this post, basically I repeat all the common hooks in all live_session calls I have in my router.

This option can become a chore really fast if I have a bunch of common hooks that I want to add, makes the router harder to read and more verbose.

A second solution would be to add a prepend? option to the on_mount/1 call, that way, I can write it like this:

    on_mount Hooks.GetLocale, prepend?: true
    on_mount Hooks.GetTimezone, prepend?: true

And with this I would get this order of execution:

  1. Hooks.GetTimezone
  2. Hooks.GetLocale
  3. {Hooks.Authentication, :not_authenticated}

The third solution would be to allow live_session (or a simplified version of that that only allows to set the on_mount option) to be defined inside another live_session, for example:

live_session :common, on_mount: [Hooks.GetLocale, Hooks.GetTimezone] do
  scope "/auth", Live.Auth do
    live_session :not_authenticated, on_mount: {Hooks.Authentication, :not_authenticated} do
      ...
    end
  end

  scope "/admin", Live.Admin do
    live_session :authenticated, on_mount: {Hooks.Authentication, :authenticated} do
      ...
    end
  end
end

IMO that would be the best approach since it will allow to scope it the same way we do with the scope macro.

What do you thing about this suggestion? Do you know another way to achieve the same?

1 Like

I wanted something similar before and this is very nicely thought-out post so apologies for the very short response: For hooks that are always required, I’m content using a single LiveHooks module that attaches and loads all the necessary hooks/assigns. Because they are all laid out in a single function, it doesn’t bother me so much that they aren’t explicitly listed in my router. While including them in MyAppWeb.live_view is not a bad option, it’s best to be in the clear that odd time yo don’t want to load them for a special-case route. It’s also where people except them to be.

1 Like

Hey @sodapopcan How would you handle cases where they have different hooks per route if they are all in a single function?

Or do you mean having a common hook just for the hooks that are required to all the liveviews?

If the later, that is my current solution actually, but I do still need to type it in every single live_session call I have in my route.

Here is my common hook for now:

defmodule CoreWeb.Router.Hooks.Common do
  alias CoreWeb.Router.Hooks

  def on_mount(name, params, session, socket) do
    with {:cont, socket} <- Hooks.Locale.on_mount(name, params, session, socket),
         {:cont, socket} <- Hooks.Timezone.on_mount(name, params, session, socket) do
      {:cont, socket}
    end
  end
end

Oh ya, that’s all I meant. I’ve actually never found myself in the situation where I have specific hooks I want in different scenarios, everything has pretty much always been global based on authentication, so I’m not actually super qualified to answer here due to lack of experience.

As for typing for every live_session, I don’t totally mind this myself as it keeps with Elixir’s “be explicit” mantra. But I don’t usually have a lot of them so again, I’m unexperienced there in terms of what that would actually be like.

Unrelated: your username stuck out and I realized it was because I finally tried Flashy yesterday :smiley: So far it’s working well but will report any feedback. It’s a nice lib, thanks!

1 Like

I think it’s a totally valid ask. Not sure which of your suggestions to favor. If the implementation is viable, the nested live_session is neat. The prepend?: true option gets the job done but could lead to a state in which it’s hard to reason about overall hook order.


@sezaru, did you find a better way to control the order of the on_mount hooks?