So, LiveView 0.20 brought an async processing API which is a thin wrapper around tasks. It’s a very nice convenience, but the main thing that intrigues me is support within live_components. This is the first time that live components have been able to receive system messages directly. Anyone who’s tried to do async data loading in a live component knows what I’m talking about.
This is great, but it’s a very narrow API. It lets you perform a single async task and get its results. I’m proposing a more generic version of this API. Currently we have
def mount(%{"id" => id}, _, socket) do
{:ok,
socket
|> assign(:org, AsyncResult.loading())
|> start_async(:my_task, fn -> fetch_org!(id) end)
end
def handle_async(:my_task, {:ok, fetched_org}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.ok(org, fetched_org))}
end
def handle_async(:my_task, {:exit, reason}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.failed(org, {:exit, reason}))}
end
but I’d like to be able to do
defmodule App.LiveOrg do
use GenServer
def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end
@impl true
def init(opts) do
:ok = Phoenix.PubSub.subscribe(App.PubSub, "org:#{opts.id}")
org = fetch_org!(id)
:ok = Phoenix.LiveView.async_update(opts.live_view_ref, org)
{:ok, %{org_id: opts.id, live_view_ref: opts.live_view_ref}}
end
@impl true
def handle_info(_pub_sub_message, state) do
org = fetch_org!(state.org_id)
:ok = Phoenix.LiveView.async_update(opts.live_view_ref, org))
{:noreply, state}
end
end
# in my live view or live component
def mount(%{"id" => id}, _, socket) do
{:ok,
socket
|> assign(:org, AsyncResult.loading())
|> start_async(:my_live_org, App.LiveOrg, %{id: id})
end
def handle_async(:my_live_org, {:update, fetched_org}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.ok(org, fetched_org))}
end
def handle_async(:my_live_org, {:exit, reason}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.failed(org, {:exit, reason}))}
end
Just to clarify what’s happening here, I’m asking for start_async
to be able to start and supervise arbitrary processes. When it does this it should pass a processes a :live_view_ref
option to the process which the new Phoenix.LiveView.async_update/2
function can take to send an async update back to the live view or live component. This allows the started process to send multiple updates, and for those updates to target the underlying phoenix live process and assign correctly (regardless of if that assign is in a live view or a live component).
Conceptually this is nearly identical to the current task based API, and I think we should keep the task based API around as a convenience on top of this API, but I’m a fan of having the full power of OTP rather than just tasks.