I'm missing :not_asked status on LiveView.AsyncResult

I’m working with LiveView.assign_async, which provides :ok, :failed, and :loading statuses. I think it would benefit from adding a :not_asked status.

The assign I’m working with, which I’ll just call state for now, only gets triggered after a user action. So at mount/3, the state is nil.

In render, I have to wrap any checks on state, like state.ok? or state.loading, with a nil check first. Here’s what I’ve tried:

Checking for state = nil

<div :if={@state[:ok?]}>Not asked, the state is nil</div>

This works fine, but when state switches to AsyncResult.loading, it throws an Access error:

(UndefinedFunctionError) function Phoenix.LiveView.AsyncResult.fetch/2 is undefined (Phoenix.LiveView.AsyncResult does not implement the Access behaviour)

Workaround 1: Wrapping Everything

One option is to wrap every state check inside a nil check:

<div :if={!is_nil(state)}>
  <div :if={state.ok?}>Success on showing state</div>
  <div :if={state.loading}>Hold on, loading...</div>
  <div :if={state.failed}>There was an error!</div>
</div>

This works but starts to get cumbersome when state needs to be checked in multiple places across the page.

Workaround 2: Initialize with :not_asked

The other approach is to initialize state as AsyncResult.failed(%AsyncResult{}, :not_asked):

<div :if={state.ok?}>Success on showing state</div>
<div :if={state.loading}>Hold on, loading...</div>

But when handling failures, you need to differentiate between actual errors and the :not_asked state:

<div :if={state.failed}>There was an error. Wait! It might just be that it wasn’t asked yet.</div>
<div :if={state.failed != :not_asked}>There was a real error</div>
<div :if={state.failed == :not_asked}>Press here to load</div>

Suggested Improvement: Add :not_asked Status

I think it would be helpful to add a :not_asked status, so you can handle these cases more cleanly. One would need to initiate the assign, but after that, it would be easier to work with.

<div :if={state.ok?}>Success on showing state</div>
<div :if={state.loading}>Hold on, loading...</div>
<div :if={state.failed}>There was an error, a real one.</div>
<div :if={state.not_asked}>Press here to load</div>

Adding a :not_asked status simplifies the logic and removes the need for repetitive nil checks. It would make the state handling in LiveView.assign_async cleaner, especially in situations where user actions trigger the state change.

A PR could look like this:

2 Likes

By the way, this is inspired by: How Elm Slays a UI Antipattern

1 Like

The pull request to introduce this logic was denied, but we gained some good insights on how to get the same behaviour with the primitives that are available now.

1 Like