Skip to content
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

Introduce a new use_async hook, that allows for setting dependencies #3609

Closed
wants to merge 2 commits into from

Conversation

Ekleog
Copy link

@Ekleog Ekleog commented Feb 20, 2024

Description

Introduce an use_async hook, that performs a computation in another task and returns the result.

(Probably) fixes #364

The main API design question is whether we want to introduce the idea of cancellation in the API or not. Adding it afterwards would likely require adding a variant to UseAsyncStatus. This being said, Yew is not stable yet, and probably at least 90% of the use cases are covered by this API, so I think it makes sense to postpone the decision after verifying that there is an actual need.

This PR is not done yet, and is here mostly to gather feedback on whether this is something that Yew developers would be interested in having upstream. I'm using it in my own code here, and thought it would likely be helpful to other people too.

The alternatives I know of are:

  • The use_async and use_async_with_options from yew_hooks. They do not support introducing dependencies that would trigger a future re-computation.
  • yewtil's LinkFuture. Not having used it I'm less familiar with it, but AFAIU it requires struct components, which is not the direction Yew development seems to be moving towards.

Checklist

Things still missing if you confirm that this PR is of interest to you:

  • I have reviewed my own code
  • I have added tests
  • Writing clear documentation
  • Fixing all of CI

Copy link

github-actions bot commented Feb 20, 2024

Size Comparison

examples master (KB) pull request (KB) diff (KB) diff (%)
async_clock 100.530 N/A N/A N/A
boids 173.547 N/A N/A N/A
communication_child_to_parent 93.072 N/A N/A N/A
communication_grandchild_with_grandparent 105.655 N/A N/A N/A
communication_grandparent_to_grandchild 101.043 N/A N/A N/A
communication_parent_to_child 89.413 N/A N/A N/A
contexts 105.821 N/A N/A N/A
counter 86.281 N/A N/A N/A
counter_functional 86.451 N/A N/A N/A
dyn_create_destroy_apps 89.275 N/A N/A N/A
file_upload 100.440 N/A N/A N/A
function_memory_game 172.253 N/A N/A N/A
function_router 349.531 N/A N/A N/A
function_todomvc 162.304 N/A N/A N/A
futures 229.101 N/A N/A N/A
game_of_life 109.982 N/A N/A N/A
immutable 189.679 N/A N/A N/A
inner_html 80.040 N/A N/A N/A
js_callback 109.576 N/A N/A N/A
keyed_list 198.589 N/A N/A N/A
mount_point 82.877 N/A N/A N/A
nested_list 114.631 N/A N/A N/A
node_refs 90.462 N/A N/A N/A
password_strength 1726.314 N/A N/A N/A
portals 93.754 N/A N/A N/A
router 318.227 N/A N/A N/A
simple_ssr 141.650 N/A N/A N/A
ssr_router 389.408 N/A N/A N/A
suspense 116.107 N/A N/A N/A
timer 88.976 N/A N/A N/A
timer_functional 97.995 N/A N/A N/A
todomvc 142.318 N/A N/A N/A
two_apps 85.590 N/A N/A N/A
web_worker_fib 136.037 N/A N/A N/A
web_worker_prime 186.757 N/A N/A N/A
webgl 82.662 N/A N/A N/A

✅ None of the examples has changed their size significantly.

Copy link
Member

@WorldSEnder WorldSEnder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that having such a hook in yew directly can be useful, especially considering the details the PR as-is gets wrong (see further comments) and indeed would most often be overlooked when reimplementing this in some third-party lib.

Thanks for the initiative!

packages/yew/src/functional/hooks/use_async.rs Outdated Show resolved Hide resolved
Res: 'static,
{
let result = use_state(|| None);
use_effect_with(deps, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By making this hook an effect, its potentially only useable on the client side, as effects don't run in SSR. Is this intended?

Copy link
Author

@Ekleog Ekleog Feb 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm so TBH I have very little knowledge of how SSR is supposed to work, and zero experience with it — I'm using yew to develop cross-platform pseudo-heavy applications rather than lightweight webapps.

Do you see use_async being useful with SSR? And if yes, is there any other hook I could use to define dependencies?

EDIT: NVM, I just saw use_memo as an alternative to use_effect_with. But I'm still not sure about what SSR is about and how that'd tie in with use_async in particular?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that perhaps you want to asynchronously fetch data on the server to render. But on second thought, this was a bit hasty because the component will be only rendered once in SSR anyway, so any resolved result will likely never make it into the component anyway if this works just similar to use_memo and just spawns the future, but then continues to render. The solution could be to just not poll the future on the server at all.

Note, because you also mention it in the other comment about use_future: Suspensions do work on SSR (see there for a small explanation on what suspensions are) and wait for the future to complete, i.e. the suspension to resolve before rendering the component into the stream.

let future = f(deps);
wasm_bindgen_futures::spawn_local(async move {
let res = future.await;
result.set(Some(res));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unconditionally setting the result means that futures from earlier renders, that happened to resolve later can override an existing result with potentially stale date. I don't think that's a good idea.

Consider the following sequence:

  • render with deps1, spawn future f(deps1)
  • render with deps2, spawn future f(deps2) - this could potentially cancel earlier jobs, but I don't recall wasm_bindgen_futures offering to cancel a spawned task so let's ignore that for now
  • f(deps2) resolves to some result2 and sets this
  • f(deps1) resolves to some result1. I would consider it unintended to then present result1 to the user. The last time the component rendered, f(deps2) was requested, which shouldn't be overwritten.

pub fn use_async<Arg, F, Fut, Res>(deps: Arg, f: F) -> UseAsyncHandle<Res>
where
Arg: 'static + PartialEq,
F: 'static + FnOnce(&Arg) -> Fut,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if F should be run as part of an effect, or just inline. Certainly the resulting future should be spawned later, but getting the future should not be effectful (running the future to completion does that!).

If you agree, please rewrite, and remove the 'static bound here.


use crate::{functional::hook, use_effect_with, use_state, UseStateHandle};

#[hook]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs documentation. In particular, this should mention use_future and when to use which of the two. Feel free to also adjust the docs there to point back here so that someone stumbling upon either can decide which one to use. And ask if you're not sure about use_future yourself.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so now I feel stupid. I had looked into yew::functional for hooks, and had totally missed yew::suspense also having a hook, that seems to be exactly what I was looking for…

So now I'm wondering, what value do you see in use_async, that would not be covered by use_future already? AFAICT it's possible to just match on the SuspensionResult instead of ?-ing it out, so I'm not seeing much value right now, but maybe I'm missing something?

(Also, looking at the implementation of it resulted in this PR, but that's unrelated)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can confirm that I was able to replace my personal use of use_async with use_future_with. Maybe the main thing to improve about use_future would be re-exporting it from yew::functional, so that it's in the prelude, and maybe to document it a bit more prominently?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value would be that use_async requires knowledge of Suspension and this might not always be what the user intends to do. Note that the following code is not the equivalent of what I'd expect use_async to do:

let future: impl Future<Result = T> = todo!();
let foo = use_future(future).ok(); // An Option<T>, sucess?!
// let foo = use_async(future); // With this PR (fixed signature without dependencies)
// These two lines are not interchangeable.

In fact, dropping the Suspension - instead of returning it from a component function - will subsequently not lead to a re-render when the future resolves! will re-render but I'm not sure if that is intentional design in hindsight. Suspension and use_future want to say: this component is not ready to render yet, wait for a bit until I tell you to try again. use_async should say: Resolve this future, but I know what to do while the data is not there yet, keep on rendering.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can confirm that I was able to replace my personal use of use_async with use_future_with. Maybe the main thing to improve about use_future would be re-exporting it from yew::functional, so that it's in the prelude, and maybe to document it a bit more prominently?

That is a bit surprising to me, too. Do these components perhaps re-render because of some other thing happening instead of the Suspension resolving?

Copy link
Author

@Ekleog Ekleog Feb 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm is it supposed to not resolve? Looking at these lines:

Is my new code relying on an unintended feature that could disappear in the next version? 😅 (Right now I don't care much because it's only example code, but I'm planning on writing a crdb-yew crate next that'll be basically implementing lots of custom async-using yew hooks, so I'd rather not make that mistake there too)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh alright, the implementation actually guarantees that your component will re-render even if you ignore the suspension. I'm not sure it should from a semantic perspective but that's not in scope for this PR. In any case, the creation of a suspension and suspend handle is really a bit useless if you don't want to suspend, so still I think this hook is worth something by avoiding useless work :)

use crate::{functional::hook, use_effect_with, use_state, UseStateHandle};

#[hook]
pub fn use_async<Arg, F, Fut, Res>(deps: Arg, f: F) -> UseAsyncHandle<Res>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convention with dependencies is to have two hooks, use_*_with and use_* one version with dependencies, the other if you don't need to have them. Please rename and implement the simpler one for consistency.

Copy link
Member

@hamza1311 hamza1311 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you describe how this differs from use_future?

@WorldSEnder
Copy link
Member

WorldSEnder commented Feb 27, 2024

@hamza1311 you might not have seen my earlier comment

@hamza1311
Copy link
Member

@hamza1311 you might not have seen my earlier comment

Yep, totally missed that, my bad

@Ekleog
Copy link
Author

Ekleog commented May 7, 2024

Considering the time I've been having recently, I think I unfortunately won't have any to come back to this PR.

Going to close this, but anyone please feel free to take it over! :)

@Ekleog Ekleog closed this May 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Consider handling async service effects through futures
3 participants