How do you tackle user notifications and other data in every LiveView?

The Task

Brace yourselves, wall of text is coming

For my current project I have a need to show notifications and small indicators (to highlight areas with new information) to users. Most of these indicators or notifications should be available/visible on any page as long as the user is logged in.

Why a discussion?

This is probably a very common thing to have/need, but searches in the forum turned up pretty old or unrelated posts. Maybe I was not good at using the search function.

I would be very interested in how you people would tackle (or have tackled) a scenario like this. While the motivation behind this post is to answer the question on how to do this for my project, I am aiming to have more of a discussion than just asking a question.

What do we need to do?

Assuming both an indicator and a popup notification are desired, it is necessary to

  1. Figure out if there are unread notifications on page load to display indicators and
  2. Subscribe and react to updates on every page for continuous updates

Out goals?

  1. Hit the database(s) as rarely as possible
  2. Manage as little state as possible - or better: none
  3. Duplicate as little code as possible
  4. Do not touch each LiveView file
  5. Keep the concept simple

Discussion: How can we do that?

Assuming all pages are LiveViews.

This is the part that I would really like to discuss.

The following outlines some possible solutions I could come up with (some of them I have never tried) and what I think the upsides and downsides are, but I might be mistaken there, in which case I would be happy to improve my understanding.

Page load

Simple/Stupid: Hit the database on every load

:slightly_smiling_face: Simple to set up: slap the database request into a Plug or on_mount and assign the result. Use the assign in your template.
:slightly_smiling_face: No state management
:neutral_face: Does not scale well as it requires unnecessary resources ā€¦ although most applications would probably be okay with this.
:slightly_frowning_face: Makes the page feel less snappy or even slow.
:slightly_frowning_face: Not that much of an argument, but: Somehow feels bad to do it like this :P.
:question: Is there a way to keep some of the data when navigating within a live_session?

Go through a cache (Agent/GenServer/ETS) to get the the database result

When a user is first active after some time, grab their data from a database, store it somewhere and do incremental updates

:slightly_smiling_face: Pretty simple to set up (on_mount)
:slightly_smiling_face: Faster after first load
:neutral_face: Trades computing for memory
:neutral_face: Possible bottleneck?
:slightly_frowning_face: State management: The cache needs to subscribe to updates and invalidate its data if needed. Not clear how long we should wait before data can be dropped and probably not trivial to keep state up to date?

Start a User twin that lives as long as the User is active (Presence)

Basically the same as the above, but with

:slightly_smiling_face: better isolation and less bottleneck-y?
:slightly_smiling_face: When using e.g. Phoenix Presence we have a clear indicator on when we can drop the twin and its data.

Nested, sticky LiveView

Kind of like the twin: Separate process, that fetches the data on initial load and can then act as a cache.

:slightly_smiling_face: Requires no lifecycle handling, as Phoenix would cover that
:slightly_frowning_face: Only useful when navigating within a live_session(?), as the nested LiveView would otherwise be reloaded as well.
:slightly_frowning_face: For each new window or browser tab we start from scratch
:question: If communication with the parent LiveView is needed, for example to update different indicators on the page, we actually do not gain much, as we still have to touch every LiveView? ā† Could make use of JS events here.

Live Updates

Subscribe each LiveView to a PubSub

:slightly_smiling_face: Pretty simply concept
:slightly_frowning_face: Lots of code copying for duplicate handle_info() and mount() content.

Subscribe on_mount, add handle_info() to use XYZ, :live_view

:slightly_smiling_face: Pretty simple setup
:neutral_face: Compiler will complain that handle_info() functions are not grouped.

Move handle_info() to separate macro/quote, place it manually next to other handle_info()

Compare to above:
:slightly_smiling_face: Compiler is fine
:slightly_frowning_face: Need to add code to each LiveView

Nested, sticky LiveView

Basically like the player in LiveBeats Ā· Phoenix Framework Have a separate LiveView that fetches the

:slightly_smiling_face: No copied code: Put all handle_info() and subscriptions in one place
:slightly_smiling_face: Simple setup: Add the LiveView to your template
:slightly_smiling_face: Popup notifications should be simple on each page with this through e.g. absolute positioned UI elements.
:neutral_face: Only useful when navigating within a live_session(?), as the nested LiveView would otherwise be reloaded as well.
:question: If communication with the parent LiveView is needed, we actually do not gain much, as we still have to touch every LiveView.

JavaScript event listener in app.js

Not entirely sure about this one, as I avoid JS wherever possible. Maybe an option is to add a JS event listener to app.js and then update specific DOM elements as needed (e.g. giving notification indicators an ID and show/hide them).

Should also work with dead views.

Separate Phoenix Channel

I have no experience with this.

Should also work with dead views.

Bonus Question/Scenario

If some of the pages users can visit (and have notifications on) are also publicly available pages and therefore can be viewed without having an actual user associated, how would you suggest to handle that? Separate LiveView? Separate Template?


:cookie: for everyone who got here without skiping :smiley:

5 Likes

We can embed a live view in the root layout, that will show up on all pages using that layout!

Using live_render

It can be used to embed a live view in a static view as well, but in that case page load will happen on every navigation.

For example, see the heartbeat indicator at the top right of the page.

Itā€™s part of the HeaderLiveView, and doesnā€™t reset on page route change. (If navigation happens by live_patch!)

Source Inspector Auth Pages

:neutral_face: Only useful when navigating within a live_session(?), as the nested LiveView would otherwise be reloaded as well. (This is my observation as well, but I donā€™t mind as Iā€™ll keep the pages that belong together, in the same session and layout anyways, probably)

As for talking to parent live view, it is coming in later versions of LiveView I guess.

4 Likes

Pretty cool example.

The whole header bar is its own LiveView? That would probably be the setup for me as well, as it shows multiple indicators.

I see there is a bell icon as well, which is probably for notifications?


I used a template embedded LiveView in a previous project, but it did not have any state at all. It was just a statically positioned button that would open a modal with a contact/bug report form. Was a very convenient approach for that!

Itā€™s a project Iā€™m making for learning.

Iā€™ll be adding notifications in that bell icon eventually, yes.

Also kind of off-topic, but your login looks like it could have been the result of phx.gen.auth at some point? What are you using for handling Google/GitHub logins? Uberauth? Pow? Something else?

I used phx gen auth, and I havenā€™t added any other library yet.

Iā€™m confused about using a library.

Perhaps Iā€™ll try to emulate login done in Livebeats to make GitHub work, or Iā€™ll just use one of the libraries with great UX.

P.S.

That red outline you see around the auth pages is not part of the UI. Itā€™s a source code inspector, that opens up a code editor on click and navigates to the page where the pageā€™s source is defined.

If Iā€™m understanding youā€™re explanation of these options, I think you might be interested in taking another look at the docs for attach_hook/4. There are both on_mount and handle_info lifecycle stages, so you should be able to define how you receive the message there. You could do something like the following in a single ā€œhookā€ module:

  1. In on_mount, when connected, subscribe to the relevant topic.
  2. In on_mount, load from db (or your cache or whatever) the initial data.
  3. In hand_info, receive the message and assign it to the socket.

The compiler does not complain about this.

7 Likes

To add on to this, you can attach a lifecycle hook via on_mount onto an entire live session/3 from your router.

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  # pipelines, plugs, etc.

  live_session :default, on_mount: MyAppWeb.InitAssigns do
    scope "/", MyAppWeb do
      pipe_through :browser
      live "/", PageLive, :index
    end
  end
  ...
end

source: Phoenix.LiveView ā€” Phoenix LiveView v0.19.5

5 Likes

Oh cool, I was not aware of this. Knowing only the approaches I listed it seemed somewhat odd to me that there is no more convenient solution, but I guess this could be the piece I was missing.
After going through the docs I have to say I was not sure how to use this approach, as the examples in the docs did somehow not make it click. But attach_hook is used in live_beats, so that is a great reference.

I am just now realizing that this is what allows me to have reusable Components that bundle their own handlers. The way I (will) use LiveView has just changed and improved dramatically :smiley: Not sure how I missed this, at it seems to be available since LiveView 0.16.0 :thinking: thanks for the hint!

Great! Yes, it was not obvious to me from the docs, either.

The live_beats example app utilizes live.html.heex under the hood, which IIRC has been deprecated. Does anyone know of an up-to-date example of this kind of setup?

root.html.heex only contains head and body, which encloses <%= @inner_content %>.

app.html.heex contains two LiveViews:

  1. A NavbarLive LiveView that would include LiveComponents for notification dropdowns etc.
  2. Whatever is the ā€œmainā€ LiveView for that route.