I have a few LiveView based forms on a single page, rendered from DeadView with live_render/3 and all of them work just fine. The problem I have is that all of them need (partially) the same data like current_user for example. As a result all of them send the very same query to the backend/DB, resulting in the same query executed multiple times for a single page. Now, I understand that I could load current_user once - before rendering the forms and pass it to all forms using session: [...] but I’d prefer not to put too much data into session. Even The Fine Documentation™ warns me not do to it, saying: “instead of storing a User struct, you should store the “user_id” and load the User when the LiveView mounts”, which is precisely what I do (having current user_id in regular session).
Is there an easy way to share a dataset between several LiveViews in such scenario? Some wrapper LiveView or something?
I’ve never used this but I think if you give your LiveViews a parent LiveView then you could use assign_newPhoenix.Component — Phoenix LiveView v1.0.11. Not sure if that has other unwanted implications for your app.
It’d be nice if there was a simple built-in solution that could also be used for sharing state between channels easily. Maybe it already exists and I’m not aware of it? I suppose you could use cachex or your own GenServer.
TNX for the suggestion. I was thinking about making some sort of a “wrapper” LiveView, yet wasn’t sure if that’s the right/best/easiest approach. The docs fragment you linked to describes almost exactly the case I have at hand though. So if nobody knows any better way, then that’s probably what I need to take a stab at. Genservers, :ets, cachex & Co. all seem a bit like shooting flies with a howitzer for such a simple thing.
You need to carefully design the communication between them as if with any other processes. For example, only the parent liveview instance will receive any url params so if you need those for the children, you’ll need to propagate them by sending messages to them whenever you handle_params in the parent (meaning the children will need to “register” with the parent when mounted).
On the other hand, you can call the parent from its children to fetch its state-assigns (e.g. the current_user) via GenServer.call/2 if you’re sure the parent has what the child needs at the time of calling it.
In this particular case I don’t expect url params to be a problem as LiveViews in question are rendered / mounted on an oldskool DeadView page and all interaction effects on that page are contained within each of those LiveViews, not affecting url/params.
Thanks a lot for the heads-up though! May come handy as I continue to migrate elements of the application to LiveView.
One question: what exactly did you mean by children having to “register” with the parent when mounted? Did you mean PubSub? Or something else? Oh, and could you elaborate a bit on the GenServer.call/2 part?
You don’t need PubSub to send messages from parent to its children. By registering the children I mean when they mount you can use the Socket.parent_pid to send parent the child’s pid. That way the parent can directly communicate back with (send messages to) them afterwards.
As for GenServer.call/2, a LV is a GenServer so you can call it from another process to return whatever you need from it by simply implementing its handle_call/3.
There is the “Sharing assigns” mechanism described in assign_new/3 docs.
When checking this out last night I noticed that I already made all of those LiveViews utilise assign_new/3 in order to employ the said mechanism. I assign the current_user struct to conn in the plug pipeline, before calling live_render/3. Double-checked that the assign is available in the conn passed to live_render/3 :
live_render(@conn, MyappWeb.DashboardFormLive)
According to the docs
“In such case conn.assigns.current_user will be used if present. If there is no such :current_user assign or the LiveView was mounted as part of the live navigation, where no Plug pipelines are invoked, then the anonymous function is invoked to execute the query instead.”
And despite clearly having the assign in place, the anonymous function is invoked, executing the query. In the logs those mounts/queries show after the
[info] Sent 200 in 4ms
[info] CONNECTED TO Phoenix.LiveView.Socket in 40µs
Transport: :websocket
[...]
as if that was executed on the second render (?)
The mount function doesn’t have anything unusual, and the assign in question look like:
def mount(_params, session, socket) do
socket = socket
|> assign_new(:current_user, fn -> Myapp.Accounts.get_user_by_id(session["user_id"]) end)
# [... a few other assigns here]
{:ok, socket}
end
In a nutshell: Forget about it. It’s effectively useless because you can’t count on it.
From the docs (the line of not being guaranteed was added Jose’ after I pointed out it should’ve been explicit):
“Assigns sharing is performed when possible but not guaranteed. Therefore, you must ensure the result of the function given to assign_new/3 is the same as if the value was fetched from the parent. Otherwise consider passing values to the child LiveView as part of its session.”
Well… so even if I wrapped all those LiveViews in a parent one, then the assign_new might still not yield the expected results, right? This would mean the whole “Sharing assigns” section is pretty much misleading and effectively useless as you mentioned? Huh…
Then I am back to square one In such case it seems like your GenServer suggestion being the next best, but:
I still wrap all of them in a parent LiveView
I move the assign_new up there so that even if it executes the query, there is only one (next to the one executed by the plug pipeline) being executed
I implement handle_call/3 in the parent that returns the value I need
In each of the children I get the assign’s value by making GenServer.call/3 to the parent
Right? Now where do I get the server argument required by the GenServer.call/3 function from? Socket has parent_pid but docs say “server can be any of the values described in the “Name registration” section”. And it is not me who starts and names the GenServer. How would you do it? Am I missing something obvious?
Do note that this is a synchronous call, so if the parent is loading the current user (or whatever else) at that moment, the child will be waiting until the parent process is ready to receive the message.
This is why I told you the children should “register” with the parent (you can use PubSub in place of `send/2, but in my view it would be an unnecessary overhead).
In my case(s) the following steps take place:
When mounting as connected the child sends its pid to the parent (using send/2)
Parent assigns the pid of each such child (“registers” them) and notifies the child it’s been registered (by sending a message to its pid, also using send/2) - this is also a signal that the parent is ready accept any synchronous calls from the child.
3.Child receives the message that it’s now registered with the parent so from that moment on it can call the parent (GenServer.call/2) to fetch whatever it needs.
OK, so I don’t need any “name” for the GensSever.call. Its PID suffices. Again - why do the docs insist on names?
Yes, that’s how I expect it to behave. I want children to wait for parent to fetch the data before they continue as they cannot continue otherwise. Parent’s whole purpose is to do just this one thing. But I agree that in general case, where the parent may be busy doing many things, “registering” is a more correct way. I’ll see where I can get with this approach. Thank you once more for your patience and explanations. I’ll take a stab at it and report back later, TNX.
That sentence is likely there because people generally don’t understand the intricacies of how LV connects and what that means to the working of assign_new. I doubt it is meant to mean that for the places where sharing assigns is implemented it would somehow be unreliable.
So for this topic specifically: I’m not sure assign_news assign sharing is implemented on live_render. Especially if you intend to share assigns between many live_rendered subsections of a page I’d suggest moving the whole page to a liveview to begin with.
That sentence was added because it was indeed misleading to suggest that assign_new can be used to propagate the assigns of a parent to its children in a consistent way.
The point raised make sense, but are entirely unapplicable to this usecase here. With live_render there is no live_navigation to begin with, that feature only exists for router mounted live views.
I’d still consider this a consistent behaviour, just one happening in a different way to the issue openers expectations.
As someone who recently implemented something like this, I can only recommend to avoid sticky liveviews if you can. It’s much harder and less flexible compared to manual solution.
By manual solution I mean a “sidecar process”: LiveViews belonging to the same user start a new process or subscribe to existing process for the user. The process loads relevant data, subscribes to PubSub and sends relevant data straight to subscribers’ dedicated LiveComponent, whose job is to render it based on current page. When data is updated the sidecar process forwards updates to subscribers. When there’s been no subscribers for a period of time, the sidecar process shuts down. You can render the LiveComponent in any template or layout.
Essentially the sidecar works like a cache, but much better.