I’m not convinced any of this is a good idea. The purpose of a process is that it’s a unit of shared memory. By trying to share memory across processes you are fighting the fundamental abstraction.
If you want to share assigns I think you should just refactor the LiveViews into LiveComponents and place them under one LiveView.
If you want isolation between them for some reason (I have yet to find good reasons for this, personally) then you should accept that loading data from the DB (or from a cache) separately is the cost of that isolation.
Of course you can structure these LiveViews hierarchically and pass data down, but then why not use LiveComponents to save some RAM while you’re at it? Your app would be structured effectively the same way.
Now that you explicitly mentioned it I found in the next paragraph a list of “supported formats”, which indeed starts with “a PID”. So it seems like my fault for not going through it thoroughly enough to get that simply pid would suffice. My bad.
For an excuse I can possibly only say that if something sends me away to “Name registration”, and the vast majority of that section is actually about – well – names and registering them, then it was easy to overlook that I don’t in fact need any names because – despite being neither a name nor something I can register – pid is also mentioned there once. So only in this sense it may be “not clear enough”. Maybe changing the wording to “server can be any of the values described in the “Name registration” section of the documentation for this module or simply the server’s PID” would be better?
I implemented those some time ago (don’t remember now exactly whether things around components were back then in current maturity state), rendered from within oldskool DeadView. They are separate forms with their own handling of changes, validations, etc. serving discrete subsystems within the application but in this case grouped on one page for centralised access. Additionally they are also used separately in their respective areas of responsibility. Do you say that since I still need to wrap them into an encompassing parent LiveView, it may make now more sense to turn them into LiveComponents? This in fact might be an option. With the only (small I assume) drawback being that I’d need to wrap each of them separately into a parent LiveView in every place I use them (on DeadView pages). Still an option I guess…
LiveComponents have a very similar API to LiveViews. The only thing they really can’t do is receive erlang messages. They can still engage in message passing through send_update, though one must be careful not to abuse that power.
I have had no problems splitting up large interactive apps into many LiveComponents under a single LiveView. I like things that way because it keeps everything in a single process, meaning: memory is shared and everything has a total order (no concurrency). Concurrency in a UI is a dangerous thing if you’re not careful, and it’s rarely necessary anyway.
But yes, I think turning the forms into LiveComponents and then rendering them in LiveViews as needed (composition, essentially) is best. If you need to render just one form, make a (simple) LiveView which does that. If you need to render several, have your LiveView do that. And so on.
For performance this is obviously better (not copying memory across processes), and in my case it allows me to couple components tightly when necessary (because they are part of the same interface), or not (if they are generic, like a special input).
Maybe someone else can comment with a counter-example, but I don’t see the value proposition for composing entire LiveViews. The only case I can think of where that makes sense is if you had a couple of totally separate applications “within” your app and you needed to render them on the same page at once, or something like that.
As a rule of thumb, my suggestion is to use LiveView instances (as opposed to LiveComponents) when one or both of the following apply:
the UC requires a segregated error/failure reporting/treatment that’s not supposed to affect the rest of the app
you want to stay comfortable using the resources (read/write) synchronously (as opposed to writing async handlers) without locking up the rest of the app
As suggested by @garrison messaging within and between LiveComponents can be dealt with send_update/3, but it comes with a serious limitation: when handling such messages you can’t decide to push_patch/navigate for it’s part of the pre-rendering (i.e. it’s too late or too early for that).
But, there’s a workaround for that too. You can send a message to your LiveView process to do the patching for you OR if you need a complete encapsulation, you can start_async the part that pushes the patch/navigation.
Good to remember, but as mentioned before, this doesn’t pose a problem for the case at hand. All interactions effects remain contained within each respective form and do not trigger any navigation out of the (DeadView) page they are rendered within. So in such specific, rether than generic case it should be fine.
OK - so in the case at hand I am going to need to have:
a LiveView that queries DB for the data pieces needed by all forms
render all the forms as LiveComponent(s) from within the LiveView above
pass the assigns down to <.live_component [...] />
Now I updated an assign in one of the forms. Let’s say I updated current_user. How do I get all other forms to get the updated assign? None of the components “know” what other components are out there, next to it so I can’t really send_update/3 to all of them. It needs to go through the parent LiveView. Thus I added
def handle_info({:update_assigns, {key, value}}, socket) do
{:noreply, assign(socket, key, value)}
end
I was under the impression these were completely separate forms, except for some global shared state (e.g. current_user, which I would not expect to be changed by a form).
If they’re actually coupled you are no longer combining them into one LiveView for performance reasons; you are now building an application. If that’s the case I would imagine the experience of trying to do that with separate LiveViews was excruciating!
Anyway, yes, you need to ship state changes back to the root so they can filter back down through the assigns of the components. There are a few ways to do this:
You can explicitly send mutations back up with send() or send_update(), as you propose. To generalize this, you can take the approach described in the docs where you pass down a function which performs the appropriate update when called.
A more advanced approach is to use PubSub closer to the data layer, e.g. broadcast a message from your context “announcing” that some object was created/updated. This approach is still primitive and I don’t love it but it does work and I have generalized it with some success in my apps. Note that there are still some cases where you want state which is truly local to the LiveView, and in that case you would still use the send() trick as you have.
Finally, the most advanced technique is to have a database which can deliver a proper stream of changes. This is not really something which exists “properly” yet, but people have been trying to parse the Postgres WAL with varied success, the most promising attempt thus far being ElectricSQL/PhoenixSync. I am skeptical of using Postgres for this but I won’t get into it. But as with the PubSub approach, there are still going to be cases where you need “local” (to the LiveView) state, no matter how advanced your database is.
I should take the opportunity to clarify: when I said “abuse that power”, sending messages to sibling components is what I was talking about. These messages should virtually always go up the tree, and then the assigns can filter back down. There are ways you could obtain the ids of sibling components, but you shouldn’t, because you will end up in trouble if you head down that path.
They are separate but they share some application level entities. They are used to configure various aspects of the same application. Some elements of those aspects are configured “per user” and stored in user’s “config” struct. Some are “per user organisation” (which is also common for them), and some are completely separate. The forms are “Live” and store their information (after validating, etc.) separately from each other. This means the value of common data can change after interaction with one of the forms.
No, it was all fine. Until I had to put them on a single page that is
Yup, that’s what I mentioned. Even if I could dynamically find what are the compatible siblings of a given component I would not like to do it because it shouldn’t IMHO be the component’s concern. We’re clear now though. Thank you.