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

Why are Component and Renderable separate traits? #339

Closed
Boscop opened this issue Jul 24, 2018 · 4 comments · Fixed by #563
Closed

Why are Component and Renderable separate traits? #339

Boscop opened this issue Jul 24, 2018 · 4 comments · Fixed by #563

Comments

@Boscop
Copy link

Boscop commented Jul 24, 2018

From https://bluejekyll.github.io/blog/rust/2018/07/22/static-web-app-rust.html

There are two traits Component and Renderable. Component implements all of the functions around creating and updating the component. Renderable implements the functions for rendering the component, view. It’s not yet clear to me why these two are separate traits and not just one, there’s possibly a good reason, but it’s not obvious to me yet.

I've been wondering why this is, too! Does someone have an answer? I'd prefer if they get merged into one trait.

In other frameworks (e.g. Halogen (PureScript)), every component has a render/view and eval/update function.

@hgzimmerman
Copy link
Member

While not a definitive answer, I think I can provide a reasonable guess.
I think some of the reasoning about the separation of the traits could lie in wanting to encourage some of the pure function/ stateless rendering typically encouraged in React and similar frameworks (somewhat relevant article: here).

I don't believe that the html! macro currently supports the usage of elements that only implement Renderable<T> (and I'm making this post without adequately checking), but conceptually, I think in the future, something like the following could be implemented:

impl Renderable<ComponentStruct> for ComponentStruct {
    fn view(&self) -> Html<ComponentStruct> {
        html! {
            <div>
                {self.stateless_struct}
            </div>
        }
    }
}

struct StatelessStruct {
    pub thing: String
}
impl <T> Renderable<T: Component> for StatelessStruct {
    fn view(&self) -> Html<T> {
         // whatever, as long as it never needs to set up callbacks that aren't present in the struct itself in relation to T
    }
}

I personally would prefer if Component has its own view() method, Renderable<T> had a render() function and the following block existed to automatically implement Renderable for anything that is a component:

impl <T> Renderable<T> for T where T: Component {
    fn render(&self) -> Html<T> {
        self.view()
    }
}

But this approach would incur an additional small performance penalty (extra function call) or binary size bloat (if it was inlined), and also impacts discoverability of the concept of encapsulating a pure rendering function within the domain of a specific struct/enum. The html! macro also impacts compile time significantly, so any additional complexity present within it does have its consequences.

@Boscop
Copy link
Author

Boscop commented Jul 25, 2018

I think some of the reasoning about the separation of the traits could lie in wanting to encourage some of the pure function/ stateless rendering typically encouraged in React and similar frameworks (somewhat relevant article: here).

The Halogen framework I mentioned (for the purely functional PureScript language) is doing exactly that. When creating a component, you provide an eval function that dispatches msgs on the component, and a render function that renders that state into html. Like here:
https://github.com/Boscop/web-view/blob/master/examples/todo-ps/src/Component/Todo.purs#L46-L47

And in that framework, both functions are part of each "component" so I think the reason that yew aims to do the same is no argument against having a view (or render) method as part of the Component trait.

But this approach would incur an additional small performance penalty (extra function call) or binary size bloat (if it was inlined)

It would inline this call and it wouldn't cause bloat compared to the current solution because each component's view method would always only be called through render (if this trait design would be used).

and also impacts discoverability of the concept of encapsulating a pure rendering function within the domain of a specific struct/enum.

How so? :)
Also, the docs would help here.

The html! macro also impacts compile time significantly, so any additional complexity present within it does have its consequences.

The design of these 2 traits doesn't change the number of times the html! macro occurs in user code.


Btw, I would prefer if both traits' view methods were also called the same (view or render but I prefer view), so that Renderable::view() calls Component::view() for the T: Component impl.
(It would be confusing for users to choose different names for no good reason.)

@hgzimmerman
Copy link
Member

I agree with all of your points, I think I was glossing over these details when responding initially.

To clarify my prior point about discoverability, I mostly make that within the current context regarding yew's current state of documentation, where there is no guide, and the last docs.rs entry that compiled properly is from 0.2.0. Eventually, that situation will improve, but I think by separating the traits, it does make it pretty clear that non-components can be rendered as well, which for beginners is helpful in avoiding the "everything rendered on page must be a component" anti-pattern.

@Boscop
Copy link
Author

Boscop commented Jul 25, 2018

Yea the docs definitely need to be improved, so that they build etc.
I think the most common use case is components that would currently need an impl of Component and Renderable, and since this is the most common use case, it makes sense to design the traits in a way that reduces the amount of code necessary to write a component. (And the new design would still allow users to define types that are only Renderable.)

Btw, could you think of a reason/usecase why you'd want to have a component that's not Renderable (but still used like a component (in html!{}) with its update method being called?

If not, I think the most ergonomic way (for yew users) would be to have a view method in the Component trait and a blanket impl of Renderable for T: Component that calls its view method.

Also, I wouldn't even mind if everything that occurs inside html!{} has to be a Component, and if there wasn't a separate Renderable trait.
Because

  1. yew supports snippets and functions can return snippets, so that covers the "types that should only be Renderable" (your example above with stateless_struct), e.g. html! { <div>{ foo(&self.foo_state) }</div> } where foo() returns a Html snippet based on the arg.
  2. You could just define a component's Message type to be () (or maybe even ! (Never) because it will never be instantiated) and there could be a default impl for the update method that just returns false (don't re-render), which you only override when your Message type isn't () (or !).
    If you define your Message type to be !, you can statically be sure that the update method will never even be called on that component, because for that, it'd have to instantiate an instance of type ! which is impossible! (A similar approach is used in PureScript's Halogen framework.)

I don't think it would be an anti-pattern. It would streamline/unify the trait design.

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

Successfully merging a pull request may close this issue.

2 participants