New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature request: live component unmount callback #2454
Comments
Something to keep in mind is that this in no way implies the live component is a process. The live component in my example is not "subscribing and unsubscribing to accounts" it is declaring when some bit of data (accounts) is or isn't currently being used. |
To get more concrete, this callback would allow this pattern: defmodule WithAccounts do
use Phoenix.LiveComponent
@impl true
def render(assigns) do
~H"""
<div>
<%= render_slot(@inner_block, @accounts) %>
</div>
"""
end
@impl true
def mount(socket) do
send(self(), {:started_using, :accounts})
{:ok, socket}
end
@impl true
def unmount(socket) do
send(self(), {:stopped_using, :accounts})
{:ok, socket}
end
@impl true
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> update(:accounts, fn
:unloaded -> Accounts.all()
accounts -> accounts
end)}
end
end
defmodule Dashboard do
use Phoenix.LiveComponent
@impl true
def render(assigns) do
~H"""
<div>
<button phx-click="toggle_accounts" phx-target={@myself}>show/hide accounts</button>
<.live_component :if={@showing_accounts} :let={accounts} module={WithAccounts} id="..." accounts={@accounts}>
<p :for={account <- accounts}><%= account.email %></p>
</.live_component>
</div>
"""
end
@impl true
def mount(socket) do
{:ok, assign(socket, :showing_accounts, false)}
end
@impl true
def handle_event("toggle_accounts", _, socket) do
{:ok, update(socket, :showing_accounts, fn value -> !value end)}
end
end
defmodule MyLiveView do
use Phoenix.LiveView
@impl true
def render(assigns) do
~H"""
<.live_component module={Dashboard} id="..." accounts={@accounts} />
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, accounts: :unloaded, accounts_use_count: 0)}
end
@impl true
def handle_info({:started_using, :accounts}, socket) do
socket =
if socket.assigns.accounts_use_count == 0 do
Phoenix.PubSub.subscribe(:my_pub_sub, "accounts")
assign(socket, :accounts, Accounts.all())
else
socket
end
{:ok, update(socket, :accounts_use_count, fn count -> count + 1 end)
end
def handle_info({:stopped_using, :accounts}, socket) do
socket =
if socket.assigns.accounts_use_count == 1 do
Phoenix.PubSub.unsubscribe(:my_pub_sub, "accounts")
assign(socket, :accounts, :unloaded)
else
socket
end
{:ok, update(socket, :accounts_use_count, fn count -> count - 1 end)
end
def handle_info({"accounts", _some_event}, socket) do
{:ok,
update(socket, :accounts, fn
:unloaded -> :unloaded
_ -> Accounts.all()
end)
end
end I'm sure there's a good deal of typos, but the general idea here is:
A neat advantage of this approach is that if the WithAccounts live component could be used without anything knowing it "needed" accounts. In this case, it would just provide a dead accounts list to its inner block. This graceful fallback is nice as it allows consumers to first implement the their view with dead data then slowly make pieces live as needed. |
Yeah, I am a bit torn on this issue. The only reason why we want to message the parent is for it to subscribe to events. We could, however, skip this if we allowed the component to subscribe to those directly but, because live components are not processes, resource management in them is tricky since, as you note, you have to both subscribe and unsubscribe to events. Furthermore, both in your solution and the one I mention above, if you have multiple components interested in those events, you can end-up accidentally subscribing to the same event multiple times. And I also have concerns about promoting data loading downstream (in components) because it is an easy way to get N+1 issues in the future. If I were solving this problem, I would make it so |
The issue with "promoting" loading to the parent is that, that parent could itself be dynamically rendered, and that parent's parent, etc. eventually you end up with the original "solution" of replicating all that dynamic rendering logic as dynamic loading/subscribing logic in the live view itself. As for N+1s, I agree this could lead to them if misused, but that's more or less a solved problem through the use of data loaders. Streams also, afaict, only provide a way to defer data loading until use. They don't solve the problem of knowing when to start/stop subscribing to events efficiently. I should clarify, I'm not really promoting this as a pattern that regular app code should use, but I think using this pattern in library code will be very powerful. I could imagine a world where live views become generic "live data" loaders, and views simply declare what live data they need. All live views on a node could share a live data cache, and views could be moved between live views without changes because all live views would know how to load the same data. Views would be portable and live views would be efficient. I don't want to confuse or conflate this feature with that theoretical future, but this feature is certainly necessary for a future like that, or any other in which we need a way to know what's being shown on the screen. |
Hey, @josevalim, I just wanted to see if you'd given this anymore thought. I'd be happy to work on a PR if it will be accepted. |
I have given more thought but I didn't reach a conclusion yet. I think we will need another pair of eyes from other maintainers. :) |
Closing this. It adds complexity, has complicate trade-offs, and it doesn't seem to be a common need. :) |
So, this feature was proposed in 2020, and shot down due to a perceived lack of need, but I think I have a good use case.
I want a live list of accounts. This list should update in realtime as accounts are created, deleted, or updated. To implement this, I will need to have my live view subscribe to account creation, deletion, and update pub sub events, load the initial accounts list, and render that list. Because rendering of this list might be complex, I'll use a functional component. The issue comes about when I have a very large live view, like an admin dashboard, and somewhere deep in this live view I conditionally render this accounts list functional component from another component which might in turn be conditionally rendered.
Ideally, the live view should only need to subscribe to account updates and load accounts when it is actually showing accounts. However because the logic for when it's show accounts is encapsulated into one or more live and functional components, this is non-trivial to do. In fact, the only way for the live view to do this would be reimplement all that rendering logic in itself and use it to inform whether or not accounts are being shown. This is, of course, not maintainable.
In practice, most live views simply load and subscribe to everything they might need up front. For large live views that may rarely need very high cost resources this is not an option.
My proposal is to add an unmount callback to live components. This would solve this problem by allowing live components to send their live view a message on mount and on unmount. With this the burden of knowing if a certain piece of data was currently needed or not could be flipped. Instead of a live view having to know when data was needed, live components could tell their live view when they needed data (on mount) and when they no longer needed data (on unmount). This would allow live views to render arbitrarily complex UIs without having to sacrifice on either performance (over loading/subscribing) or maintainability (reimplementing logic in the live view to dynamically determine what needs to be loaded).
The text was updated successfully, but these errors were encountered: