How to call a method or dispatch a Msg on a child element?
Or is the only way to cause a child to do something to change an attribute/property that is passed to it?
In my use case, the main app element wants to update the state of the child element after receiving new data over websockets, and ONLY that child element's DOM should be re-rendered!
How can I update a child element's state, so that only that child's DOM is re-rendered?
Currently, it updates the whole app's DOM every time a ws state update is received, which is unnecessary because every ws msg only affects a specific child element. So I want to be able to dispatch direct msgs on a child so that only that child's DOM gets re-rendered. Similar to how PureScript's Halogen framework allows querying child components:
Here is an example of an action query, which doesn't return a value ("child, do X").
Here is an example of a request query, which returns a value ("child, tell me X").
(In Halogen, queries can be used both for commands (actions) and requests (returning values from child to parent), they don't trigger a re-render of the parent's DOM. The query will be dispatched by the child's own eval method (like update in yew) and will only cause the child's DOM to be re-rendered if its own state changes as a result of handling this query.)
If I pass the state updates down through attributes/properties, it's suboptimal in many ways:
true from update (ShouldRender) JUST to be able to pass this state to the child (yew only passes properties down to children when the parent re-renders its own DOM, right?), which would re-render the WHOLE parent (app) DOM, but only the child's state should change, and ONLY the child's DOM should be re-rendered.I think the fact that currently, my whole app's DOM is re-rendered on every websocket message that's received (it's receiving msgs at ~30 fps) is the cause of the high CPU usage (https://github.com/DenisKolodin/yew/issues/351) (even though each ws msg should only affect state change in 1 small child (small meaning "small DOM surface" compared to the whole app) and should also only trigger a re-render of that small child's DOM).
And the fact that the parent has to keep clones of all children's states around, just to be able to pass them down to the children as properties during DOM re-rendering also means that the app uses much more memory than it would use, if it could dispatch msgs on child elements directly, and move the data instead of cloning.
EDIT: Also, doing the equivalent of a Halogen-like request query (with value returned from child to parent) in yew would involve even more steps and would obscure the communication even more! The steps are:
Msg enum that represents a response of the request from the child (wouldn't be necessary in Halogen. Querying the child can be done 'inline' in the parent's eval (update) method and does NOT disrupt the control flow / reading flow. Just like ajax requests in Halogen, whereas in Yew they require another Msg case with handling code, like in Elm. In Halogen, they can also be done 'inline' like writing futures code.)child_req: Option<ChildReq>.childreq: Option<ChildReq> and onchildreq: Option<Callback<()>> to the Props and Model of the child's struct. childreq and onchildreq from Props to Model in Model::create and Model::update.Model::create and Model::update for when props.childreq is Some(req), perform the action and respond by calling onchildreq to return the value back to the parent.update method of the parent, write the handling code for the response from the child. Note that the code that sent the request to the child and the code that processes the child's response are separated (different Msg variant handler branches in the match statement) which makes it harder to read what's going on, ESPECIALLY when the parent has a lot of children that it needs to query, which is quite common in real-world apps! (E.g. in the frontends I'm writing for my job, where I'm using Halogen.)While I can't guarantee that this will solve your problem, have you tried using an agent(s) to mediate message passing after your WS returns a response?
Basically:
A workflow would work as follows:
I have code for this scenario accessable from this issue: https://github.com/DenisKolodin/yew/issues/301
You could use the above solution or you could handle websockets in every relevant component (difficult to implement), or just stick the WS in an agent, and have the agent itself dispatch messages to different receivers depending on the response. This WS agent would replace the multitude of sender agents that would be required if you took the sender+receiver approach.
Thanks, but this issue is more fundamental than my WS situation, the general issue that needs to be solved in Yew is:
We need to have a way to update child state without having to change parent state and re-rendering parent DOM.
The agent approach doesn't scale, when using it for every instance of this.
Parent-Child (up & down) communication (as opposed to hierarchy-ignoring communication through external agents) is the most basic need in a frontend framework and we need to find a better solution than just passing properties down & forcing the parent to re-render its DOM.
I asked on reddit if anyone had any ideas how we could do it similarly to Halogen:
https://www.reddit.com/r/rust/comments/99hr4j/yew_ideas_for_typesafe_childquerying_and/
I think the most promising approach (given Rust's type-system limitations) is the one I described here.
This would allow us to talk to children directly in update() like:
self.child(ChildSlot::ChildA).update(child_a::Msg::Foo(..));
When we instantiate it in view() like:
html! { <MyChild #ChildSlot::ChildA: prop=val, /> }
Which is almost as convenient as in Halogen, except it requires a runtime type-check during downcasting whereas in Halogen it's all checked at compile-time.
And then we could also query children with responses like:
let resp = self.child(ChildSlot::ChildA).query(child_a::Msg::GiveMeSomeAnswer);
Wouldn't you agree that this kind of child querying would be way more convenient? :)
Considering that most communication in apps is between parent & children (up/down).
I left some impl details out here (e.g. update()'s signature would change, it currently returns ShouldRender) but I can write down more details if we can agree that we want to go in this direction..
Btw, I encourage everyone to become familiar with the Halogen framework, to get ideas how we can make Yew more convenient to use. Currently, Halogen is much nicer to use than Yew (mostly because of this issue with parent-child communication but also because Halogen supports inline/async-await handling of queries/ajax etc.) but I wish Yew would become equally convenient, so that we can use it on large-scale apps without missing the convenience of Halogen..
@Boscop This seems to be an interesting topic, but unfortunately I don't understand Halogen or PureScript (or Haskell which it's similar with).
It seems to me that Vue's methods is similar to Halogen's query algebra - is this correct? If I understand correctly, Vue's methods has the same functionality except it doesn't handle return value when used as event handlers.
Random note: Vue's change tracking model doesn't require any code deciding on "re-render" or not; thus I wonder if Halogen queries has any additional benefits than performance. While Vue is very different from React or Yew, would you call Vue's design convenient?
@ishitatsuyuki It seems to be a similar mechanism as in Polymer, but it is not a query algebra, it's just a way to specify (by name) which method should handle an event. But it is completely weakly typed..
(Btw, in Polymer, you can call methods on child widgets directly (which can return values) without triggering a re-render of the parent's DOM, by referring to them via their (local) id, e.g. if you have <span id="name"> you can call this.$.name.foo() in the parent).
Btw, it seems that draco (another Rust wasm web framework) allows updating child widgets' state without forcing a re-render of the parent's DOM:
https://github.com/utkarshkukreti/draco/blob/469ff07ce9fd3f27efd8be3563ec418b518047ee/examples/counters.rs#L63
Because in draco (unlike in Yew), child widgets are normal members of the parent struct type, so they can be directly addressed! (Similar as in Elm)
@DenisKolodin I think it would make sense to also do this for Yew! So that child widget's state can be updated without triggering a re-render of the parent's DOM and it would also allow children to be queried about their state (without triggering any re-render because it would not cause view() to be called, so it would work without passing down callbacks through properties (which would trigger a re-render of the parent and all its children) and without requiring to declare any additional struct members ONLY for parent-child communication).
I think this would be the best solution for the current design of Yew, because it solves all the sub-issues of this issue.
What do you think? :)
I also think, if we do this, update should be renamed to something that also fits with querying, such as eval, because querying child components would be possible through this, e.g.:
self.btn.eval(ChildMsg::IsOn(|on| MyMsg::HandleIsOn(on));
This will basically pass a callback to the child's update/eval method directly (without requiring any boilerplate struct members/props) and the child's update/eval method will call this callback with the requested value as argument.
This example (querying a toggle button whether it's "on") is taken from here:
https://github.com/slamdata/purescript-halogen/blob/e51eee0afd4f9816a64242f9c4a7b67ffc157e7b/examples/basic/src/Button.purs#L15
https://github.com/slamdata/purescript-halogen/blob/e51eee0afd4f9816a64242f9c4a7b67ffc157e7b/examples/basic/src/Button.purs#L53-L55
You may be asking "why can't we just call self.btn.is_on()?". It would technically be possible, BUT we want to allow the child to also modify its own state when it handles queries, and re-render its DOM when its state changed, but currently, only update() can trigger a DOM re-render.
I proposed a solution for this in https://github.com/DenisKolodin/yew/issues/435
If we go with that solution, every component would own/be a State<MyState> instead of just MyState, and State<T> would be a wrapper type that detects changes to its contents and automatically triggers a DOM re-render if its contents (the component's state) changes.
Then we could also allow child querying (that mutates child state) through normal methods like self.btn.set_on(true), because any change to the child's state will be noticed by the wrapper and if necessary, a DOM re-render of this child will be triggered.
I think this would actually be a better solution because it's
HandleResult(R) case to the parent's Msg enum and implementing the handler for it, breaking locality of code flow/readability)State<T> wrapper to auto-trigger DOM re-renders, people will STILL call methods on children (it can't be forbidden, in this scenario where children can be directly addressed because they are members), and if these methods change child state, the child would not get re-rendered (because re-rendering only happens when update() returns true but update() doesn't even get called) so people would wonder why the DOM does not reflect the child's state (BAD).So the best way (IMO) would be to go with both suggested solutions (having children as members of components, and https://github.com/DenisKolodin/yew/issues/435) because they work well together.
Missing label:
There is a way to do this as shown in https://github.com/yewstack/yew/blob/master/examples/nested_list/src/app.rs
I've created an issue to write some documentation about this approach here: https://github.com/yewstack/yew/issues/905
Is there a way to pass a reference of the actual component instead that a link? I would prefer to not import the Msg of the receiver in each sender.
Instead of doing:
RECEIVER
pub Struct Receiver {...}
Struct Props {weak_link: WeakLink<Receiver>, ...}
pub enum ReceiverMsg {MyMsg, ...}
___________
SENDER
use receiver::ReceiverMsg::MyMsg
Struct Props {receiver_link: ComponentLink<Receiver>, ...}
self.props.receiver_link.send_message(MyMessage)
Do something like:
RECEIVER
pub Struct Receiver {...}
Struct Props {...}
enum ReceiverMsg {MyMsg, ...}
impl Receiver {
pub fn send_message() -> () {self.update(MyMessage);}
....
}
___________
SENDER
Struct Props {receiver_link: Rc<RefCell<Option<ComponentLink<Receiver>>>, ...}
self.props.receiver_link.send_message()
I don't think there will be a big advantages using the second solution maybe just a bit more readable, I'm asking just out of curiosity.
If both the first and the second pattern are viable, is there a better one?
@Fi3 I think the second pattern is possible but it would be quite a big effort and I'm not sure the benefit would be big enough.
Another pattern you could use is From<SenderType> for ReceiverMsg. So something like: self.props.receiver_link.send_message(SenderType.into()) would be possible. I use this pattern in my own side project
We could make this pattern even easier with a small API change: https://github.com/yewstack/yew/issues/932
@Fi3 Yes, it's possible by introducing your own trait, it doesn't even need to be in Yew.
Most helpful comment
@ishitatsuyuki It seems to be a similar mechanism as in Polymer, but it is not a query algebra, it's just a way to specify (by name) which method should handle an event. But it is completely weakly typed..
(Btw, in Polymer, you can call methods on child widgets directly (which can return values) without triggering a re-render of the parent's DOM, by referring to them via their (local)
id, e.g. if you have<span id="name">you can callthis.$.name.foo()in the parent).Btw, it seems that draco (another Rust wasm web framework) allows updating child widgets' state without forcing a re-render of the parent's DOM:
https://github.com/utkarshkukreti/draco/blob/469ff07ce9fd3f27efd8be3563ec418b518047ee/examples/counters.rs#L63
Because in draco (unlike in Yew), child widgets are normal members of the parent struct type, so they can be directly addressed! (Similar as in Elm)
@DenisKolodin I think it would make sense to also do this for Yew! So that child widget's state can be updated without triggering a re-render of the parent's DOM and it would also allow children to be queried about their state (without triggering any re-render because it would not cause
view()to be called, so it would work without passing down callbacks through properties (which would trigger a re-render of the parent and all its children) and without requiring to declare any additional struct members ONLY for parent-child communication).I think this would be the best solution for the current design of Yew, because it solves all the sub-issues of this issue.
What do you think? :)
I also think, if we do this,
updateshould be renamed to something that also fits with querying, such aseval, because querying child components would be possible through this, e.g.:This will basically pass a callback to the child's
update/evalmethod directly (without requiring any boilerplate struct members/props) and the child'supdate/evalmethod will call this callback with the requested value as argument.This example (querying a toggle button whether it's "on") is taken from here:
https://github.com/slamdata/purescript-halogen/blob/e51eee0afd4f9816a64242f9c4a7b67ffc157e7b/examples/basic/src/Button.purs#L15
https://github.com/slamdata/purescript-halogen/blob/e51eee0afd4f9816a64242f9c4a7b67ffc157e7b/examples/basic/src/Button.purs#L53-L55
You may be asking "why can't we just call
self.btn.is_on()?". It would technically be possible, BUT we want to allow the child to also modify its own state when it handles queries, and re-render its DOM when its state changed, but currently, onlyupdate()can trigger a DOM re-render.I proposed a solution for this in https://github.com/DenisKolodin/yew/issues/435
If we go with that solution, every component would own/be a
State<MyState>instead of justMyState, andState<T>would be a wrapper type that detects changes to its contents and automatically triggers a DOM re-render if its contents (the component's state) changes.Then we could also allow child querying (that mutates child state) through normal methods like
self.btn.set_on(true), because any change to the child's state will be noticed by the wrapper and if necessary, a DOM re-render of this child will be triggered.I think this would actually be a better solution because it's
HandleResult(R)case to the parent'sMsgenum and implementing the handler for it, breaking locality of code flow/readability)State<T>wrapper to auto-trigger DOM re-renders, people will STILL call methods on children (it can't be forbidden, in this scenario where children can be directly addressed because they are members), and if these methods change child state, the child would not get re-rendered (because re-rendering only happens whenupdate()returnstruebutupdate()doesn't even get called) so people would wonder why the DOM does not reflect the child's state (BAD).So the best way (IMO) would be to go with both suggested solutions (having children as members of components, and https://github.com/DenisKolodin/yew/issues/435) because they work well together.