Assign_async and liveview streams

I tried to combine the new async loading and assigning the result to a stream.
The following is working. But my question is:

Is there a prettier way?

Getting the data:

|> start_async(:get_events, fn -> fetch_events(location) end)}

Assigning the data to a stream while returning an AsyncResult with empty data:

def handle_async(:get_events, {:ok, fetched_events}, socket) do
    dbg()
    %{events: events} = socket.assigns

    {:noreply,
     socket
     |> assign(:events, AsyncResult.ok(events, []))
     |> stream(:events, fetched_events)}
  end

What’s the point of this line? Why are you pulling out events from the assigns (not sure what you’re putting in there in mount) and assigning them as an async result? If you’re trying to use the component, wouldn’t you have assigned it as an async result in the loading state initially? But having an assign and a stream for the same thing seems wrong.

Yeah, that seems unnecessary to me as well.

If your intention was to ensure that the template has something to work with before handle_async populates the stream, you could assign a new empty stream in mount that gets populated in handle_async.

def mount(socket) do
  {:ok, 
    socket
    |> stream(:events, [])
    |> start_async(:get_events, fn -> fetch_events(location) end)}
end

def handle_async(:get_events, {:ok, fetched_events}, socket) do
  {:noreply, stream(socket, :events, fetched_events)}
end

By default, calling stream/4 on an existing stream will bulk insert the new items on the client while leaving the existing items in place.

And this way your template can just use @streams.events rather than juggling and switching between @events and @streams.events.

The idea behind this is to not assign the resulting data to the socket but using the recommended liveview stream functionality.

Perhaps I’m doing it the wrong way here or I misunderstood how to handle this.

I would love to see a stream_async function though

And I have to assign at least the AsyncResultto handle it properly in the HTML:

<.async_result assign={@events}>
  <:loading>
    [...]
  </:loading>
  <:failed :let={_reason}>
    [...]
  </:failed>
    [...]
</.async_result>```

But I do have to take care of AsyncResult for the HTML part somehow, don’t I?

You don’t need an assign that is an AsyncResult in the first place, which means you don’t need to use the <.async_result ...> component. I don’t think a stream will work with the async_result component.

You can handle the empty stream or use a simple atom or string assign to hold the loading state.

You’re right.

But I started with with the normal assign_async (just to play around with the very new feature) and then decided to use streams on top of that.

Just to discover there is no support and I have to do the handling on my own using start_async and handle_async.

And the async_result component is still working by ignoring the returned data (which is set to an empty list) and accessing the stream instead:

 <tbody id="events" phx-update="stream" class="">
   <%= for {id, event} <- @streams.events do %>
     [...]
   <% end %>
  [...]

Overall I like the new assign_async very much.

Thx for all the help.

For anyone find this thread in the future, it is similar to https://elixirforum.com/t/support-async-operations-with-liveview-streams/59863/8.

Personally, I found it to be okay to use both AsyncResult, start_async and stream in the same LiveView together.

AsyncResult and the async_result component make loading and error states easy to represent, and can serve to carry ancillary data like counts or other book-keeping.

The stream, then, is solely responsible for shipping data to the client efficiently.

3 Likes

Hello,

I stumbled across the same challenge and wrote an article Adding stream_async() to Phoenix LiveView.

I also provided a hex package live_stream_async with which provides you with stream_async/4 macro that you can use like this:

  use LiveStreamAsync

  def mount(%{"location" => location}, _, socket) do
    {:ok,
     socket
     |> stream_async(:hotels, fn -> Hotels.fetch!(location) end)
    }
  end
7 Likes