Multiple LiveView. Share state across them via common socket

Hello :wave:
I followed some interesting thread , thread2 & others regarding state-management with non-trivial UI situations (eg a forum or a shopping ecommerce with an ubiquituous cart) and a likely candidate to good practice is using PubSub or similar channel to store state.

My final use-case is very similar to a shopping cart, but I here isolate the problem to see if what I’m trying to do is possible and how.

# Router
live "/", PageLive
live "/second", SecondLive
# both at root.html.leex & live.html.leex
<%= inner_content %>
--- PageLive
# at "render" function  ( also tried live_patch)
<%= live_redirect "SecondPage", to: Routes.live_path(@socket, LivesocketsWeb.SecondLive) %>
---- SecondLive
# at "render"   ( also tried live_patch)
<%= live_redirect "Go to FirstPage", to: Routes.live_path(@socket, LivesocketsWeb.PageLive) %>

# at "handle_info"
# --> some logic to write to socket
socket = assign(socket, number: Enum.random(1..5000))

Then I’m interacting this way with the UI

  1. Go to /
    -> Fires up PageLive PID<0.XXX.0>

  2. Go to /second via link at PageLive
    -> Fires up SecondLive
    -> Kills PID<0.YYY.0> from :observer app as far as I can see.

  3. After 3 seconds SecondLive sends itself (0.YYY.0) a new value
    -> assign new value into socket, eg assigns = %{ ..., number = 444}

  4. Go to / via button to land on PageLive again.
    -> Kills PID<0.YYY.0> that was created by SecondLive
    -> Now this creates a new PID<0.ZZZ.0>) that has no access to the previously modified socket with number

Question: Other than using PubSub is there a way to keep the same socket (ie its assigned data) alive during route transitions to other LiveViews?

This is very much a gap in my understanding of the socket lifecycle when routing around liveviews, so any help highly appreciated, thank you!

Repo here https://github.com/git-toni/nested-lvs

1 Like

Each LiveView has it’s own process, and the process gets killed when navigating away from it, like your example shows. The only way to keep some state between them is to persist it in another process.

That makes sense I guess :smile:
I misunderstood from Chris’s message here that there is an architecture such that makes the process survive the navigation

<%= live_render(@conn, MyChatLive, ...) %> in your root layout would give you an isolated LV that survives live navigation of the main LV from the live route. We replace everything inside the main LV on navigation, so anything outside of it won’t be touched.

But that’s not what he meant probably?

Right, that’s not what he meant. He was saying that the process for MyChatLive would survive navaigation because it’s in root.html.leex. It has nothing to do with the socket being shared across multiple LiveViews while navigating.

Ok so I my understanding gap is bigger than I thought :smile: :this-is-fine-meme:

The way I reasoned about it is that each LiveView is and individual process (as usual) whose internal state
includes a socket structure. Hence if rendering it at root.html.leex allows it to survive routes navigation, I figured I could use it as a source-of-truth socket for all the rest of LVs to consume/write-to.

In that case - if it’s not a socket - can I utilize its features to share state all across the “child” LVs?

If so:

  • How does its mount look like? ie. it seems to include socket in its params
  • How does its render look like?

Thanks truly :slight_smile:

That’s where your thinking starts to go down the wrong path. Remember that LiveView is built on top of the functionality Phoenix.Socket and Phoenix.Channel provide. Your socket endpoint is defined in AppWeb.Endpoint. Each LiveView process is a channel and the client subscribes to the topic. If you have window.liveSocket = liveSocket in your app.js, run liveSocket.enableDebugging() in your browser’s console. You can see LiveViews subscribe to their topics and diffs come over the wire when socket.assigns changes.

All that being said, you can’t share a socket with different LiveViews. You can, however, use stateful and stateless LiveComponents to pass down assigns to children, similar how you would pass down props to a React child.

1 Like

Thanks!

So based on your insights and what’s at Slack I tried to then place a LiveComponent at the root.html.leex to act as a common source of truth which forwards data to all “child” LiveViews.

Something like

# root.html.leex
 <%= live_component @socket, TruthComponent do %>
    <%= @inner_content %>
  <% end %>

Where

defmodule TruthComponent do
  use Phoenix.LiveComponent

  def mount(socket) do
    IO.puts("TruthComponent mount")
    {:ok, assign(socket, :myvalue, 120)}
  end

  def render(assigns) do
    ~L"""
    <%= @inner_content.(myvalue: @myvalue) %>
    """
  end
end

Just to test if PageLive and SecondLive would be able to access myvalue.
Unfortunately I cannot tie it up together properly at all.

Also architecting in a way were the LiveComponent is the parent and the LiveViews are its children that receive info from it feels like I’m messing up the roles - and that it should be organized
LiveView -> LComponent not viceversa.

keep some state between them is to persist it in another process

Hence your initial suggestion sounds like what I should be aiming at, a separate dedicated OTP process that would allow one to keep&share state from/to all LVs. It feels like i’m swimming against LV/LC’s flow fundamentally.

Regardless I’ll be happy to learn from some implementations/snippets if someone landed the aforementioned approach :smile:

Hence your initial suggestion sounds like what I should be aiming at, a separate dedicated OTP process that would allow one to keep&share state from/to all LVs.

I think that’s the way to go.This way, not only can you share state across live views, but you can easily share it across your entire app.

Check out this thread: Channels - Where to store state? for some details on how you might go about it. Specifically, this post by @sasajuric describes a couple straightforward approaches.

1 Like

Nope, not possible :slight_smile: You can have a single LiveView pass down assigns to LiveComponents, but that’s it. I think we are starting to go in circles here, maybe let’s shift focus. What are you trying to accomplish?

2 Likes

Thanks @Most as well for the informative link :slightly_smiling_face:

Nope, not possible

Yep I think that sums it up, I’m probably trying to do something with these tools which misses their very design currently.

The final real use-case in my case is basically as follows; conceptually an e-commerce with a shopping cart:

  • Cart value is kept in browser’s localStorage so user can resume on next visit, which is sent upon page landing via JS
let liveSocket = new LiveSocket("/live", Socket, {params: {
                                                    _csrf_token: csrfToken,
                                                    cart: localStorage.cart
                                                    }})
  • 1 x LiveComponent CartComponent that keeps the state of the cart & all its handlers (add-item, remove-item, etc) and receives the localstorage copy from parent LV upon mount -> render. Also sends updates to parent LVs when cart events arrive; hence being the source of truth for the cart.

  • 1 x LiveView Home. Makes use of CartComponent at the top and can add/remove to it.

  • 1x Liveview ItemPage. Makes use of CartComponent at the top and can add/remove to it.

  • 1x LV Checkout. Makes use of CartComponent to list items and add/remove to it.

Conceptually I realized I was looking for something similar to an in-memory router (~ to react-router's SPA MemoryRouter) but on the server :smile: .
That way I only have to resume once from localStorage (maybe even checking against a persistent copy of the cart in the backend), and from then on all the navigation from LV to LV just keeps that cart in memory.

Thanks tho, this was very helpful!

1 Like

Hey man

I just wrote a very lengthy reply to another thread rambling about how we are using LV in our company right now…

I think I should have rambled about it here, but instead please read this (skip ahead if you are not interested)

2 Likes

:scream: thank you!

I was finding myself saying “yes! this!” while reading your post :smile:
Really happy to see more advanced phoenix LV users like yourself already solving these types of patterns; and do let me know if I can help somehow in that effort.
I am just starting to understand what I wanted really so all the necessary pieces/roles are not yet clear in my mind. But I’ll give it further thinking cause I’d love to use an architecture like that :slightly_smiling_face:

One question I had at first is how to point all URL to the same entry-point(ie master LV/source-of-truth) but I reckon one can pattern-match to it as a route? As per state, live-store felt like a good starting point to part of the solution.

What stage are you at to land that pattern?

We are already using it in production, to great success.

Maybe that was a little misleading, you can use the LV routing - you just need different “routers” or “parents” for the routes, because you probably want to render different templates/LVs.

So the “Router” is just the parent LV for a routed (in phoenix router) URL.

Oh wow, congrats then :slightly_smiling_face:

:crossed_fingers: for a non-business-critical open-sourcing of the logic someday : D

Ok both of those together make my head explode a little bit, I need to give it more thinking.
I was imagining something like


# at router.ex
live "/:route", RouterLive

and then

# At RouterLive module
def mount (...) do
  # Load some stuff from the landing request
end
def handle_params(%{"route" => route },...) do
    {:noreply, assign(socket, :route, route)}
end
def render(assigns) do
   ~L"""
     <%= if @route =="first-page" %>
         <%= live_render @socket, FirstPageLive, ... %>
     <% else %>
         <%= live_render @socket, SecondPageLive, ... %>
     <% end %>
   """
end
  # <-- Then here all the handle_event thingies from stateful LiveComponents

Is that even close?

Close enough - the routing is done by LiveView, and they provide the live route.

So lets say you have a simple twitter clone - called twooter. You want a “twoots” route as index to all twoots the user is subscribed to, and an “accounts/:account_id” route to change settings.

live "/twoots", TwootsLive
live "/accounts/:account_id", AccountsLive

TwootsLive and AccountsLive are your parents (the longer I talk about this, the less I think we should call it Router, maybe LiveRoute)
TwootsLive takes care of the params, and renders a template, same as AccountsLive.
TwootsLive is a twoots manager, you can have X amounts of children, as long as you subscribe them to the parent (which makes the initial loading a little slower, because all those LVs have to mount, but browsing on the “twoots” page would be fast, because it’s processes telling each other about state change, loading their new state concurrently.)
AccountsLive would look very different, it could be only a single subscribed LV, to edit your password for example - in this case, I’d even say, don’t use a nested LV, just do everything in AccountsLive

Oh I see so if I understood your example correctly the AccountsLive in your use-case is somewhat unrelated to our discussion, right? You mentioned it for contrast.

Whereas your TwootsLive would do something similar to my RouterLive above? With the use-case difference that each of TwootsLive's child is of the same “type”(say SingleTwoot), whereas at RouterLive each of the children is a completely different “page”/LV, which in any case is not very relevant beyond the product perspective.

Good points on mount-time performance (not sure how i’d measure it yet, but good to keep in mind) and also making sure to subscribe all children LV to their parent LV. :slightly_smiling_face:
Thanks!

I guess I should clarify that most apps probably would not need to use this pattern - you can create perfect apps with just one liveview, maybe using live components… We just started using this pattern because we had complex logic on different pages, but wanted to be able to drop in all those parts on different pages - mix and match if you want.

1 Like