yewstack / yew

Rust / Wasm framework for creating reliable and efficient web applications
https://yew.rs
Apache License 2.0
30.54k stars 1.42k forks source link

How to send message between components inside one app? #270

Closed madmaxio closed 6 years ago

madmaxio commented 6 years ago

In the two apps example, there are two activators which are used for sending messages. So if I understand correctly all components must share main app Msg type? Or there is other way?

hgzimmerman commented 6 years ago

It should also be possible to have your root component pass a callback to one of its children via props, which when activated, would cause the main component to change some state that another one of its children rely upon, causing a state change in that child. This works OK in some situations, but fails to be ergonomic when you have a deeply nested component structure, or if you want to neatly encapsulate state within components, and not pass everything via props.

Alternatively, you could set up your own custom service that is attached to the Context and that holds onto a callback. When you instantiate the component that would be receiving the messages, you would register a callback with the service. Then, when you want to send the message from another component, you would call context.yourservice.send(YourServiceMsg::Variant), causing the service to pass that message variant to the receiving component via the registered callback (callback.emit()).

The latter would likely contain some code that looks like the following:

Receiving component

fn create(_: Self::Properties, context: &mut Env<Context, Self>) -> Self {
    ...
    let callback = context.send_back(
        |service_msg: YourServiceMsg| {
            Msg::from(service_msg)
        },
    );
    context.your_service.register_cb(callback);
    ...
}

Sending component

fn update(&mut self, msg: Msg, context: &mut Env<Context, Self>) -> ShouldRender {
    match msg {
        Msg::SendMessageToOtherComponent => {
            context.your_service.send(YourServiceMsg::Variant)
        }
    }
    false
}

Your Service

pub struct YourService {
    // This could be a Collection type if you wanted to support multiple receiving components,
    // but this would cause a memory leak if you didn't find a solution to remove
    // the callback from the service when the component is destructed.
    // Maybe returning a Task from the register_cb fn that when it drops, the service would delete the corresponding callback?
    callback: Option<Callback<YourServiceMsg>> 
}
pub enum YourServiceMsg {
    ...
}
impl YourService {
    fn send(&mut self, msg: YourServiceMsg) {
        if let Some(cb) = self.callback {
            cb.emit(msg)
        }
    }
    fn register_cb(&mut self, callback: Callback<YourServiceMsg>) {
        self.callback = Some(callback)
    }
}
madmaxio commented 6 years ago

Great post, thank you! I will try test this.

hgzimmerman commented 6 years ago

Before you go ahead and implement my suggestion, I would like you to know that the typical arrangement for message passing between parent and child components is defined in my first paragraph. The example I typed up should only be used for the rather rare edge case where the components that need to communicate sit far apart in the view "tree".

So if you just have a component that is the direct child of another component, go with my first suggestion. ~I'm on a phone at the moment, but I imagine that at least one of the examples demonstrates this parent<->child message passing and I'll try to edit my post to link to it later.~

EDIT: The custom components example shows off the typical child to parent communication.

https://github.com/DenisKolodin/yew/blob/master/examples/custom_components/src/lib.rs#L69 The main Model instansiates a Button and creates a callback that performs an action in the parent.

https://github.com/DenisKolodin/yew/blob/master/examples/custom_components/src/button.rs#L15 The Button component accepts a callback. If the values passed to the Button as props changed in the Model, the change() method will be called for the button, allowing you to define how the Button component would change in response.

https://github.com/DenisKolodin/yew/blob/master/examples/custom_components/src/button.rs#L42 When the <button> element is clicked, the Button will emit a value to the callback, causing the update() method to be called in the main Model.

madmaxio commented 6 years ago

Cross component message passing works great now!

Thank you for this amazing work! I think Yew is best frontend experience so far at the moment, even compared to most popular js/ts frameworks.

Still I've got a couple more question:

Inside the worker, does some api exist to see who is connected to the bridge?

Idiomatic way to send inner messages just inside one component is the agent way also ? (or activator or similar thing should be used?)

hgzimmerman commented 6 years ago

To sort of answer your first question: As far as I'm aware, there isn't an API to see what other components are connected to an agent, but you can implement something like that yourself.

You can see that fn handle(&mut self, msg: Self::Input, who: HandlerId) takes a HandlerId as one of its parameters. The HandlerId is just a wrapper around a usize if I remember correctly, so you can't determine what the original component was. What you can do, is save the HandlerIds when a component connects. Then, for some inputs, you could broadcast to all components instead of just responding to the one provided for you in handle(). I did this recently in a routing library I'm writing: here

If you wanted to get fancy with it, you could categorize the IDs into distinct sets by sending a message to the agent that tells it to move it out of the subscriptions hashset and into a more specific one or Option slot. It would conceptually be prone to programmer error (you may forget to send the categorization message after creating the bridge), but you could use this pattern to accomplish seeing what is connected at any time.

I don't fully understand your second question. For intra-component communication, you just use the Component::Message. If you want to chain or send multiple messages to the Component's reducer (update method), one of your messages can wrap another message in either a Box or Vec respectively. Example here, although I expect this example to change in the near future to no longer use this pattern

madmaxio commented 6 years ago

For intra-component communication, you just use the Component::Message.

True, but how to send it just from the code? For example, what I want to do is:

Msg::Foo => { let data = 1 + 2; something_sending_inner_message(Msg::Bar(data)); }

hgzimmerman commented 6 years ago

I think what you want is something like:

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Msg::Foo => {
                let data = 1 + 2;
                self.update(Msg::Bar(data));
                false
            }
            Msg::Bar(_data) => {
                // Do something with data
                true
            }
        }
    }
madmaxio commented 6 years ago

AH, I thought calling update is not really valid option! Thx!

austinvhuang commented 3 years ago

I'm finding this discussion helpful 3 years on.

@hgzimmerman what happened to the custom_components example? I'd like to have a look, but it seems to have disappeared leaving only the pub_sub example.

Magicloud commented 3 years ago

Sorry post in this closed issue. I wonder if there is already a built in way to do this task. Is it Scope?

madmaxio commented 3 years ago

THere is agent functionality now, check some examples or docs. If you will have any questions post here again.

14 июня 2021 г., в 05:43, 王世达 @.***> написал(а):

Sorry post in this closed issue. I wonder if there is already a built in way to do this task. Is it Scope?

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/yewstack/yew/issues/270#issuecomment-860328388, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACCRBG5JDTVCF4M76YWUZHTTSVUE3ANCNFSM4FDJMELA.