Skip to content

Improve ergonomics of async integration #4020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
chrivers opened this issue Apr 19, 2025 · 2 comments
Open

Improve ergonomics of async integration #4020

chrivers opened this issue Apr 19, 2025 · 2 comments
Labels
enhancement New feature or request

Comments

@chrivers
Copy link
Contributor

Feature Request

This is a spin-off from #4011 ("Async closure for "onclick": spawns into the aether").

Related issue: #3481

When interfacing with a rest api, and targeting web, there's really no other choice than using an async http client. This means lots of async integration in event handlers.

In general, this can work:

rsx! {
    div {
        onclick: move |_| {
            async move {
                // ...
            }
        }
    }
}

However, it's not that ergonomic to use:

1. The "magic" async support works by returning a future from the event handler

This means there's no way to use an async block in the middle of the event handler. The entire "bottom half" (or the whole) of the event handler must be async.

2. There's (seemingly?) no way to .await the async block

This makes it extra difficult to make things happen in a certain order.

For example, I tried to make a button component that displays a spinner while the .onclick is running. Conceptually, something like this:

#[component]
pub fn SpinnerButton(onclick: EventHandler<MouseEvent>, running: Signal<bool>) -> Element {
    rsx! {
        button {
            onclick: move |evt| {
                running.set(true);
                onclick.call(evt);
                running.set(false);
            }
        }
    }
}

This seems reasonable, no?

Well, if onclick is given as an async move block, it's wrapped in something that causes it to be spawned, but not awaited in a blocking-like way. The result is that .call() exits immediately, and so the SpinnerButton concept appears broken.

This is pretty surprising, and I haven't been able to find a workaround of any kind.

Suggestion

In #4011, @ealmloff had this comment:

This is interesting. Async event handler were originally added for just elements, but they were later expanded into components to make behavior more consistent.

It seems reasonable to return a status object that implements IntoFuture so you could await the result.

This would be a good issue to spin out from this thread.

As a workaround, you can accept a closure that returns a boxed future explicitly with Callback<EventHandler, Pin<Box<dyn Future<Output = T>>>

The workaround is certainly a neat idea, but it does come with some problems:

  • The component would then only be able to accept async closures
  • There's still no way to tell when the handler is done running

I think there's a design problem with the way async closures are handled right now.

In other words:

  callback.call();

Should wait until the closure is done running, no matter if it's sync or async.

I'm not sure how exactly to implement this, but that would solve this entire issue.

Anyone who explicitly wants to spawn a background task, can already do so with the spawn() helper.

I can't really see any downside to the blocking behavior, but there's plenty of advantages.

Thoughts?

@ealmloff
Copy link
Member

ealmloff commented Apr 21, 2025

To clarify, with the callback you need to await the result of the call like this. The rest of the closure will only run when the await is finished:

#[component]
pub fn SpinnerButton(
    onclick: Callback<MouseEvent, Pin<Box<dyn Future<Output = ()>>>>,
    running: Signal<bool>,
) -> Element {
    rsx! {
        button {
            onclick: move |evt| async move {
                running.set(true);
                onclick(evt).await;
                running.set(false);
            }
        }
    }
}

I can't really see any downside to the blocking behavior, but there's plenty of advantages.

The browser runtime is largely single threaded which means we cannot block the main thread without freezing the whole page. If this is implemented, we would need to return a future from the maybe-future event handler call so you could await it when you call the event handler like in the code snippet above

@chrivers
Copy link
Contributor Author

Thank you for explaining the issue, I think I see the problem more clearly now.

I'm not sure how the internal mechanics work. It certainly seems like async tasks are supported, but that's all on a single thread. Is that correct so far?

Hm, I was wondering if there was some way to make the hypothetical waiting-for-completion task.call() not stall.

In pseudo-code, is it possible to call a function that runs "other things", until completion. Kind of like this rough sketch:

impl Event {
    pub fn call(&self) -> {
        magical_async_reactor_core.run_stuff_until_done(self.future);
    }
}

I know that's a rough sketch, but do you see what I mean?

I'll admit, I have no idea if that's possible, or feasible. It's very hand-wavy, but it would make for an easy interface for programmers.

Alternatively, would it make since for event handlers to always be async? That would also make integrations smoother.

But in the end, your suggestion for a workaround is pretty solid. It's a bit verbose, but very manageable with a type alias.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants