intendednull / yewdux

Ergonomic state management for Yew applications
https://intendednull.github.io/yewdux/
Apache License 2.0
319 stars 31 forks source link

Intermediating for Server #29

Closed alilee closed 2 years ago

alilee commented 2 years ago

Thanks for this crate. I love the economy of your implementation - very impressive re-use of a small number of core concepts. It is hard to get my head around it, but can definitely appreciate the art.

I would like to make a Store like Relation<X> (a hashmap of uuids to X's) which has a Reducer which speculatively applies the change to the local Store but also makes an async Request of the Reducer message to a remote server API (as a mutation) which would respond with the updated/persisted X (or maybe an error based on validation).

Any thoughts about an example which puts the Store between yew and a server? Do you think it would be possible? My attempt has run into weird ownership issues with the Store update inside a spawn_local inside Reducer::apply.

intendednull commented 2 years ago

Hi @alilee, you're not alone! Async during a reduction is a popular request, and we now have future support on master (https://github.com/intendednull/yewdux/blob/master/examples/future/src/main.rs), however the Reducer trait specifically hasn't received this attention yet.

Adding a new, async version of Reducer is not a problem. I'll get that working as soon as possible!

To be completely honest, async reductions have been a bit of a blind spot for me. I've always only updated application after a request, however I do see the utility of an async context during a reduction. Thanks for bringing it to my attention, will notify when ready!

For now I suggest a simple workaround:

enum Msg {
    // The message send from your app
    Action,
    // The message you get after the server completes 
    ServerResponse(ServerResponse),
}

impl Reducer<MyStore> for Msg {
    fn apply(&self, state: Rc<MyStore>) -> Rc<MyStore> {
        match self {
            Msg::Action => {
                let state = { /* changes to local state */ };

                spawn_local(async move {
                    let response = client.request(..).await;
                    Dispatch::<MyStore>::new().apply(Msg::ServerResponse(response));
                });

                state
            },
            Msg::ServerResponse => {
                let state = { /* handle response */ };
                state
            }
        }
    }
}

As you've already discovered, you won't be able to mutate state inside of the async context. Instead I suggest creating a new dispatch that applies a dedicated message to handle the server response. This effectively places the async stuff between reductions, instead of during.

If you'd like further assistance with the particulars of your implementation, please feel free to reach out on the yewdux channel of the Yew discord. I (or anyone from that amazing community) can provide a more reactive support for you whenever needed!

alilee commented 2 years ago

Thanks very much for the clues - I'm seeing the way forward. I'll try and put a PR together for a very small example myself.