Using callbacks for LV components messages replies?

Sometimes I have components that sends some message to another component and it needs to get a message back.

For example, let’s say I have a table component and a filter component, when I select a filter and click Apply, the apply button will change to its loading state and send a message (via send_update) to the table component to reload its data with the new filter applied.

The thing is, if I only use LV’s built-in loading capabilities (phx-click-loading), the Apply button will just have the loading state until its event returns, meaning that there will be a “time window” where the button will not have the loading state anymore and the table didn’t reload the data yet (more noticeable if there is some latency).

To remedy that, I was thinking about sending a callback to the table component, something like this:

# Code from the filter component
def handle_event("apply_filter", filters, socket) do
  on_done = fn -> send_update(@myself, op: :done) end

  send_update(Table, id: "table", filters: filters, on_done: on_done)

  {:noreply, assign(socket, loading?: true)}
end

Now, at the table component, I can do something like this:

def update(%{filters: filters} = assigns, socket) do
  %{on_done: on_done} = assigns

  # do stuff with filters and load new data
  ...
  
  # after we are done with loading stuff, call the on_done callback
  on_done.()

  {:ok, ...}
end

Finally, back at the filters component, I can handle the callback:

def update(%{op: :done}, socket), do: {:ok, assign(socket, loading?: false)}

So, basically, with this, my Apply button will stay in the loading state until the data is finished loading in the table component regardless on how much time that takes.

Now, my question is, is this a good way to handle these cases? Is there a better way?

I also like using that method.

BTW, send_message → send_update?

Yep, just fixed in the main post, thanks for letting me know :slight_smile:

Found a downside to using this strategy.

If you plan to send the callback inside a start_async call and before that call finishes start another start_async (for example, you have buttons in a table to sort, you click in one column sort button, then right after that you click in another column sort button), then the old process will be killed and be replaced with a new one, meaning that the old callback will never be called.