larshei

larshei

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:

Most Liked

sa-mm

sa-mm

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.

codeanpeace

codeanpeace

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 v1.2.5

derpycoder

derpycoder

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.

Where Next?

Popular in Discussions Top

vans163
So useless benchmarks aside, Its possible to write a webserver that can serve 300k requests per second (perhaps more with optimizations)....
New
Ankhers
Just a little information upfront. Generally speaking, if I feel like I need to either break a pipe chain or use an anonymous function in...
New
nburkley
AWS re:Invent is on at the moment with some interesting announcements. One new feature in particular is the Lambda Runtime API for AWS La...
New
AlexMcConnell
The reason that Rails is as popular as it is is because it’s very easy for relatively inexperienced developers to get a lot of work done....
588 19652 166
New
Crowdhailer
I’ve been hearing much about the new formatter and it’s something I have been keen to try. I find examples buy far the most illuminating...
248 19327 150
New
mbenatti
Following https://github.com/tbrand/which_is_the_fastest |> https://raw.githubusercontent.com/tbrand/which_is_the_fastest/master/imgs...
New
IVR
Hi all, I’ve seen a number of related threads in the past, but I’d still be very curious to hear an up-to-date opinion on this topic. I...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
wmnnd
The Go vs Elixir thread got me thinking: Would it be too hard to implement a simple mechanism for creating Go-style static app binaries f...
New
scouten
I’m looking for a host for the server part of a small (personal) side project that I’m working on. It’s currently written in Node.js and ...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39467 209
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54120 245
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement