We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Performing asynchronous work is common in LiveViews and LiveComponents. It allows the user to get a working UI quickly while the system fetches some data in the background or talks to an external service, without blocking the render or event handling. For async work, you also typically need to handle the different states of the async operation, such as loading, error, and the successful result. You also want to catch any errors or exits and translate it to a meaningful update in the UI rather than crashing the user experience.
Async assigns
The assign_async/3
function takes the socket, a key or list of keys which will be assigned asynchronously, and a function. This function will be wrapped in a task by assign_async, making it easy for you to return the result. This function must return an {:ok, assigns}
or {:error, reason}
tuple, where assigns is a map of the keys passed to assign_async. If the function returns anything else, an error is raised.
The task is only started when the socket is connected.
For example, let's say we want to async fetch a user's organization from the database, as well as their profile and rank:
def mount(%{"slug" => slug}, _, socket) do
{:ok,
socket
|> assign(:foo, "bar")
|> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)
|> assign_async([:profile, :rank], fn -> {:ok, %{profile: ..., rank: ...}} end)}
end
Warning
When using async operations it is important to not pass the socket into the function as it will copy the whole socket struct to the Task process, which can be very expensive.
Instead of:
assign_async(:org, fn -> {:ok, %{org: fetch_org(socket.assigns.slug)}} end)
We should do:
slug = socket.assigns.slug assign_async(:org, fn -> {:ok, %{org: fetch_org(slug)}} end)
See: https://hexdocs.pm/elixir/process-anti-patterns.html#sending-unnecessary-data
The state of the async operation is stored in the socket assigns within an Phoenix.LiveView.AsyncResult
. It carries the loading and failed states, as well as the result. For example, if we wanted to show the loading states in the UI for the :org
, our template could conditionally render the states:
<div :if={@org.loading}>Loading organization...</div>
<div :if={org = @org.ok? && @org.result}>{org.name} loaded!</div>
The Phoenix.Component.async_result/1
function component can also be used to declaratively render the different states using slots:
<.async_result :let={org} assign={@org}>
<:loading>Loading organization...</:loading>
<:failed :let={_failure}>there was an error loading the organization</:failed>
{org.name}
</.async_result>
Arbitrary async operations
Sometimes you need lower level control of asynchronous operations, while still receiving process isolation and error handling. For this, you can use start_async/3
and the Phoenix.LiveView.AsyncResult
module directly:
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
start_async/3
is used to fetch the organization asynchronously. The handle_async/3
callback is called when the task completes or exits, with the results wrapped in either {:ok, result}
or {:exit, reason}
. The AsyncResult module provides functions to update the state of the async operation, but you can also assign any value directly to the socket if you want to handle the state yourself.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.