Pauan / rust-dominator

Zero-cost ultra-high-performance declarative DOM library using FRP signals for Rust!
MIT License
999 stars 63 forks source link

Update examples to separate view and state update! #12

Open limira opened 5 years ago

limira commented 5 years ago

I do not have a look into dominator, just have a quick look at example/stdweb/counter. Follow up what I've said about my concern in a discussion in rustwasm/gloo, I open this issue instead of saying this there because it's about dominator only.

At least, I think dominator should provide a way to move code that modifies the state out of the renderer. If it is already available, I think the examples should be updated to demonstrate that.

limira commented 5 years ago

To make it clear, this proposal is not about forbidding state change in the renderer. But just the ability move the mutating codes out of the renderer and the renderer still need to make a call to the moved-out code.

Pauan commented 5 years ago

Thanks for creating this issue!

This is a deep and nuanced topic. In particular, Rust's general lack of global variables means that the typical Elm-style of architecture is more complicated.

So I have a question for you: since you don't want any state changes inside the render function, where do you think those state changes should go instead? Into a global variable? Into a trait?

limira commented 5 years ago

I'm not experienced enough to say exactly how I want it to be, especially for a crate that I never use before. If you are happy with how it is right now, just ignore this. I may use this crate in the future, then, I may have some thoughts.

limira commented 5 years ago

I just do an experiment here. It may be one of the ways to solve this. Admittedly, I am not sure if there is any problem with my approach because I have little knowledge about JS.

Pauan commented 5 years ago

Yeah, I saw that. I think it's a cool experiment, though I don't think it requires a new framework: it should be possible to do something similar with dominator.

limira commented 5 years ago

I don't think it requires a new framework

That's why I do not release it on crates.io yet. And do not bother to register a placeholder for mico.

Pauan commented 5 years ago

Untested, but something like this should work:

pub trait Component {
    type Message;

    fn update(&mut self, message: Self::Message);

    fn render(&self, messages: Messages<Self>) -> Dom;
}

pub struct Messages<A> {
    state: Rc<RefCell<A>>,
}

impl<A> Messages<A> where A: Component {
    pub fn push(&self, message: A::Message) {
        let state = self.state.borrow_mut();
        state.update(message);
    }
}

impl<A> Clone for Messages<A> {
    fn clone(&self) -> Self {
        Messages {
            state: self.state.clone(),
        }
    }
}

pub fn component<A: Component>(c: A) -> Dom {
    let messages = Messages {
        state: Rc::new(RefCell::new(c)),
    };

    let state = messages.state.borrow();

    state.render(messages.clone())
}

Now you can define components like this:

pub struct State {
    // ...
}

impl State {
    pub fn new() -> Self {
        // ...
    }
}

enum Message {
    Foo,
    Bar,
}

impl Component for State {
    type Message = Message;

    fn update(&mut self, message: Self::Message) {
        // ...
    }

    fn render(&self, messages: Messages<Self>) -> Dom {
        html!("div", {
            .event(clone!(messages => move |_: MouseClick| {
                messages.push(Message::Foo);
            }))
        })
    }
}

And then you can create and use those components like this:

html!("div", {
    .children(&mut [
        component(State::new()),
    ])
})

This seems like a pretty good system. It's reasonably light-weight, and it does have a clean separation between updating and rendering.

It avoids a lot of the problems of heavy-weight components, because it's using a light-weight trait + Rc + RefCell system.

Unfortunately it doesn't solve the "push messages to higher components" problem, this is only for self-contained components.

Pauan commented 5 years ago

By the way, you can use the above Component system immediately with dominator, you don't need to wait for me to add anything.

I've tried hard to make dominator flexible enough so that systems like Component can be added without any changes to dominator itself.

limira commented 5 years ago

pub trait Component

Unfortunately it doesn't solve the "push messages to higher components" problem, this is only for self-contained components.

In Mico, the trait Component above is actually the trait Application of which only one instance is allowed. If a user want to make reusable components (just a function or normal struct), they can just pass the argument main: &WeakMain<A> around, and register an event with it:

Button::new()
    .text("Click me")
    .on_click(main, move |_| Msg::Foo),

then, every messages are triggered on the app-state. Such a component cannot have it own state. But with the help of futures-signals, it's possible for an app to put everything (data) in a single app-state with very little performance lost.

As I understand, your solution may behave the same by wrapping everything in one single Component and passing the cloned of messages: Messages<Self> around. It just feels a little less elegant than Mico, especially the way events are registered, because this is just an add-in solution.

Pauan commented 5 years ago

Such a component cannot have it own state. But with the help of futures-signals, it's possible for an app to put everything (data) in a single app-state with very little performance lost.

The purpose of components isn't for performance (in Elm, creating components does not increase performance).

The reason for components is to create encapsulated reusable libraries. This requires a clean separation of state. The library cannot use your app's state, since then it won't be a reusable library.

But let's say you aren't interested in reusable libraries, and you write all your code yourself. Even then, you can't just write your app as a single huge 50,000 line component, so you need to split it up into multiple components. And state encapsulation is really critical to splitting things up in a way that is manageable.

These are difficult problems, which I've given a lot of thought to. I don't have a good solution, but I don't think the solution is to have state separation only for the application. I think it's important to be able to create separate encapsulated components.

especially the way events are registered, because this is just an add-in solution

It's true that a specialized framework (like mico) will be able to provide a smoother experience compared to a flexible framework like dominator. But with mixins I don't think the situation is too bad.

Untested, but something like this should work:

#[inline]
fn push_message<A, B, E, F>(messages: &Messages<Self>, f: F) -> impl FnOnce(DomBuilder<B>) -> DomBuilder<B>
    where A: Component + 'static,
          B: IEventTarget + Clone + 'static,
          E: ConcreteEvent,
          F: FnMut(E) -> A::Message + 'static {
    let messages = messages.clone();

    #[inline]
    move |dom| dom
        .event(move |e: E| {
            messages.push(f(e));
        })
}

And now you use it like this:

html!("div", {
    .apply(push_message(&messages, |_: MouseClick| Message::Foo))
})

I think I should add in macro support to html, then you would be able to write an event macro which is even more convenient:

html!("div", {
    .event!(messages, MouseClick => Message::Foo)
})

I think it's possible to have both flexibility and convenience.

tiberiusferreira commented 5 years ago

Hello, sorry if this is not the right place to ask this. I'm trying understand how to create reusable "components".

As far as I know, https://github.com/Pauan/rust-dominator/issues/2#issuecomment-384463126 is the recommended way of doing this currently, right?

However, I still don't understand how parent-child communication (and vice-versa) is addressed by that or even the Component traits discussed here. As I understand, the Msgs would be sent from the current Component to itself.

Do you have any thoughts on how to implement such communication? I'm specially interested in

and doing so in a way which is fully encapsulated.

Pauan commented 5 years ago

As I understand, the Msgs would be sent from the current Component to itself.

Yes, that's correct. I mentioned earlier in this thread that that is a big limitation of the Component trait.

Do you have any thoughts on how to implement such communication?

I've talked more about that here.

I don't have any particular recommendations right now. There's so many different ways to handle communication (with so many different tradeoffs).

So I think all of the various options should be experimented with before we decide on a "blessed" option. Thankfully, dominator is flexible enough that this experimentation doesn't require changes to dominator itself.

So the experimentation can happen quite quickly, it just requires somebody to do it, and try out various options in a real project.

Passing "props" down to child components

As a general rule, that is handled by either passing values into a struct, or passing arguments to the render method.

Notifying parent of child events or child state changes

This is where things get tricky. You can use streams, queues, event dispatchers, traits, all sorts of options.

Passing the parent a real DOM ref of the child (can be useful for example for scrolling the child view or focusing it etc)

This is of course doable right now: dominator gives you access to raw DOM nodes, and you can use the above-mentioned methods to pass it upwards.

I don't think it's a great idea though. I think it's better to encapsulate that in some sort of API, rather than exposing internal details.

limira commented 5 years ago

The purpose of components isn't for performance (in Elm, creating components does not increase performance).

I only have very limited experience with React (few weeks), and some with Yew (two or three months) before starting my Simi (which is an incremental virtual DOM framework). Never use any other frameworks (include Elm).

But my thought is that, in virtual DOM approach, a framework could just perform rerender vDOM + diffing + apply changes only for the component that its state did change, not the whole app's vDOM. That means a better performance for the framework. I don't know how Elm work to say anything about it.

We are now actually being helped by signals (not in the vDOM world), a state-less component, just need a reference to the required data from the single-big-state of the app, still able to trigger the right part of the code to update the view. But it's only my thought, not sure how to implement it yet.