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

How to call a method or dispatch a Msg on a child element, WITHOUT re-rendering parent DOM? #350

Closed
Boscop opened this issue Jul 30, 2018 · 10 comments
Labels

Comments

@Boscop
Copy link

Boscop commented Jul 30, 2018

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:

  • It obscures what's happening: This WS state update should not be part of the state of the parent, but of the child. But the parent would have to add it to its state just to be able to pass it down as a property. And using properties for child communication obscures the fact that there's just a communication happening here!
  • It uses much more memory, because this child state is huge (several MB) and should only exist once. If the parent could pass it to the child directly, it could move instead of having to clone one instance for itself.
  • It forces the parent to return 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 (#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:

  1. Declare a new variant in the parent's 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.)
  2. Declare a struct member on the parent's Model struct, that represents the request: child_req: Option<ChildReq>.
  3. Add new members childreq: Option<ChildReq> and onchildreq: Option<Callback<()>> to the Props and Model of the child's struct.
  4. Write the manual code for moving childreq and onchildreq from Props to Model in Model::create and Model::update.
  5. Write the handler code in 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.
  6. In the 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.)
@Boscop Boscop changed the title How to call a method or dispatch a Msg on a child element? How to call a method or dispatch a Msg on a child element, WITHOUT re-rendering parent DOM? Jul 30, 2018
@hgzimmerman
Copy link
Member

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:

  • Your parent component creates a bridge to a sender agent.
  • Your child component creates a bridge to a receiver agent.
  • The sender agent has a bridge to the receiver agent.
  • The receiver agent keeps a list of the components that have connected to it.
  • The sender and receiver's message types are specified generically, and will only forward messages of the same type.

A workflow would work as follows:

  • A response arrives on the WS.
  • Your parent component handles it and dispatches a message to the relevant sender. It does not rerender.
  • The sender dumps the message on it's paired receiver, which then sends the message to the child component.
  • The child component handles the message and rerenders.

I have code for this scenario accessable from this issue: #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.

@Boscop
Copy link
Author

Boscop commented Aug 28, 2018

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..

@ishitatsuyuki
Copy link

@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?

@Boscop
Copy link
Author

Boscop commented Nov 18, 2018

@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 #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

  1. more convenient to call methods instead of creating callbacks (which would also require adding a new HandleResult(R) case to the parent's Msg enum and implementing the handler for it, breaking locality of code flow/readability)
  2. if we don't use this 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 #435) because they work well together.

@samuelvanderwaal
Copy link
Contributor

Missing label:

  • question

@jstarry
Copy link
Member

jstarry commented Jan 31, 2020

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: #905

@Fi3
Copy link
Contributor

Fi3 commented Feb 7, 2020

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?

@jstarry
Copy link
Member

jstarry commented Feb 10, 2020

@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

@jstarry
Copy link
Member

jstarry commented Feb 10, 2020

We could make this pattern even easier with a small API change: #932

@Boscop
Copy link
Author

Boscop commented Feb 11, 2020

@Fi3 Yes, it's possible by introducing your own trait, it doesn't even need to be in Yew.

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

No branches or pull requests

6 participants