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

Components v2 (2) #1961

Merged
merged 39 commits into from
Aug 25, 2021
Merged

Components v2 (2) #1961

merged 39 commits into from
Aug 25, 2021

Conversation

ranile
Copy link
Member

@ranile ranile commented Jul 19, 2021

Description

Fixes #830
Closes #1646 (supersedes it)
Closes #1560 (removes NeqAssign)

Changes

  • New component trait
  • Remove ComponentLink - Scope is used directly
  • Component's Context - passed to all component methods
  • change method is renamed to changed
  • changed is only called on prop changes
  • Properties no longer require Clone to be implemented.
  • Properties must now implement PartialEq
  • Remove NeqAssign
  • Internally use Rc for props instead of cloning

Checklist

  • I have run cargo make pr-flow
  • I have reviewed my own code
  • I have added tests

@ranile
Copy link
Member Author

ranile commented Jul 19, 2021

The API is mostly in it's final form. I would prefer if anyone, especially maintainers, reviewed it before I go ahead and fix the documentation and examples as that would make the PR bigger, harder review and change. There are a couple of thing I would note:

  1. I don't really like the yew::html::Context name. Context is confusing with the Context API but I can't think of a better name. Any ideas?
  2. Props are now Rced. This removes the clone bound and prop clones happening within Yew. That's well and good but I'd prefer an approach based on references and lifetimes (that's outside the scope of this PR, just mentioning it)

PS: This PR is marked as draft because it isn't ready to be merged yet.

CC: @siku2, @cecton

@github-actions
Copy link

github-actions bot commented Jul 21, 2021

Visit the preview URL for this PR (updated for commit 4c188eb):

https://yew-rs--pr1961-comp-rewrite-vj5zauo3.web.app

(expires Tue, 31 Aug 2021 16:36:57 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@ranile
Copy link
Member Author

ranile commented Jul 21, 2021

This PR is ready for review and I'm marking it as such.
All the tests pass except for 2, which I've pointed out in my comment above (#1961 (comment)). I don't understand why that's happening and it would be great if anyone can help me out.

@ranile ranile marked this pull request as ready for review July 21, 2021 14:22
Copy link
Contributor

@mc1098 mc1098 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Website Documentation that mentions ComponentLink, neq_assign, or uses the old syntax of the Component trait:

  • docs/advanced-topics/optimizations.md
    • #neq_assign
      No longer needed
    • #using smart pointers effectively
      refers to Component::change
    • #Pure Components
      mentions neq_assign
  • docs/concepts/components/callbacks.md
    • #Callbacks
      examples use self.link.[callback_fn]
  • docs/concepts/components/children.md
    • #General usage
      example stores props in Component struct + view(&self)
    • #Advanced usage
      As above (A/A)
    • #Enum typed children
      A/A
    • #Optional typed child
      A/A
  • docs/concepts/components/properties.md
    • #Derive macro
      Properties must implement PartialEq instead of Clone
  • docs/concepts/function-components/attribute.md
    • Initial section
      No need to explicitly state that the parameter needs to implement Properties and PartialEq
      "The parameter type needs to be a reference to a type which implements Properties and PartialEq (ex. props: &MyProps)." Can remove the PartialEq? as all Properties must be PartialEq.
    • #Example
      Example props derive Clone
    • #Generic function components
      Example props and T derive Clone
  • docs/concepts/html/classes.md
    • #Components that accept classes
      example stores props in Component struct + view(&self)
  • docs/concepts/html/components.md
    • #Nested
      examples stores props in Component struct + view(&self)
    • #Nested Children with Props
      example stores props in Component struct + view(&self)
  • docs/concepts/html/elements.md
    • #DOM nodes
      example uses old view syntax view(&self)
    • #Listeners
      examples uses ComponentLink + old Component syntax
  • docs/concepts/html/list.md
    • #Iterators
      example uses self.props syntax
  • docs/concepts/components.md
    • #Create
      refers to ComponentLink and example uses old syntax
    • #View
      example uses old view syntax
    • #Rendered
      example uses old syntax
    • #Update
      example uses old update syntax
    • #Change
      whole section needs updating
    • #Associated Types
      Last paragraph should probably mention that properties are passed through Context?
  • docs/concepts/contexts.md
    • #Struct components
      refers to ComponentLink and example uses old syntax
  • docs/more/development-tips.md
    • #Jetbrains IDEs
      template using old syntax
    • #VS Code
      A/A

impl<COMP: Component> Context<COMP> {
/// The component scope
#[inline]
pub fn link(&self) -> &Scope<COMP> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think to a new user this might seem odd, calling link and getting back a Scope 🤔 Not sure if it's worth having an alias again or whether just expanding on that doc comment is enough. It might be that I'm splitting hairs about something that will always be a bit vague to a new user anyway.
It made me pause so thought I'd just add this note here in case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this should be renamed. I thought the same while renaming but left it that way. I think that type aliases when they point to the same thing, under a different name only do nothing but cause confusion/indirection.

Comment on lines -33 to -50
/// ```
///# use yew::{Html, Component, Properties, ComponentLink, html};
///# struct Model;
///# #[derive(Clone, Properties)]
///# struct Props {
///# prop: String,
///# }
///# impl Component for Model {
///# type Message = ();type Properties = Props;
///# fn create(props: Self::Properties,link: ComponentLink<Self>) -> Self {unimplemented!()}
///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
///# fn change(&mut self, _: Self::Properties) -> bool {unimplemented!()}
///# fn view(&self) -> Html {
/// html! {
/// <Model prop="value" />
/// }
///# }}
/// ```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of having doc examples for the Component trait's associated types, however, I don't think the examples removed were particularly useful so I'm in favour of removing them here and letting another PR cover adding more useful ones.

website/docs/concepts/components/properties.md Outdated Show resolved Hide resolved
examples/boids/src/main.rs Outdated Show resolved Hide resolved
examples/crm/src/add_client.rs Outdated Show resolved Hide resolved
examples/node_refs/src/input.rs Outdated Show resolved Hide resolved
examples/todomvc/src/main.rs Outdated Show resolved Hide resolved
examples/todomvc/src/main.rs Outdated Show resolved Hide resolved
examples/todomvc/src/main.rs Outdated Show resolved Hide resolved
packages/yew-macro/src/lib.rs Outdated Show resolved Hide resolved
@ranile
Copy link
Member Author

ranile commented Jul 22, 2021

@mc1098 I made the requested changes, please take a look again.

PS: thanks for taking the time to look through the docs that I missed to update.

Copy link
Contributor

@mc1098 mc1098 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the website doc points still stand (I've updated the check list above).

website/docs/concepts/components.md Outdated Show resolved Hide resolved
@ranile
Copy link
Member Author

ranile commented Jul 23, 2021

@mc1098, hopefully that should be all.

I'm also working on a tutorial, which should complement the docs (though it uses function components). I think we should update the docs to be more precise after that.

@mc1098
Copy link
Contributor

mc1098 commented Jul 23, 2021

I think we should update the docs to be more precise after that.

Yep, enhancing the docs should be different PR(s) and I think this PR should just correct the docs relevant to it's own changes, which I think is now done 🎉.

All the tests pass except for 2

I'm going to have a look at this, need a better understanding of this part of the code anyway :D

@ranile ranile mentioned this pull request Jul 23, 2021
3 tasks
@ranile
Copy link
Member Author

ranile commented Jul 23, 2021

I'm going to have a look at this, need a better understanding of this part of the code anyway :D

I ended up fixing those myself. I had to store the children prop inside the component but it works. I'm not sure if that's the best way to handle this though but that's how it was before. Context optimizations are outside the scope of this PR so I guess they can come in later.

packages/yew/src/context.rs Outdated Show resolved Hide resolved
@mc1098
Copy link
Contributor

mc1098 commented Jul 23, 2021

I think this might hit on a common use case though - a Component's rendering might be conditional on only part of it's Properties value so PartialEq doesn't necessarily state whether a component should render, thus the need for changed. I think, however, in these cases, we are forcing users to still hold onto old prop values to compare with the new.

I wonder whether the changed method could be defined like this:

fn changed(&mut self, _old_props: &Self::Properties, _ctx: &Context<Self>) -> ShouldRender

I think this should be possible since we have access to the old props within the update event so we could just do something like this:

UpdateEvent::Properties(mut props, node_ref, next_sibling) => {
    // When components are updated, a new node ref could have been passed in
    state.node_ref = node_ref;
    // When components are updated, their siblings were likely also updated
    state.next_sibling = next_sibling;
    // Only trigger changed if props were changed
    if state.context.props != props {
        std::mem::swap(&mut state.context.props, &mut props);
        state.component.changed(props.as_ref(), &state.context)
    } else {
        false
    }
}

If you agree... (looks at the documentation 👀) then I'm sorry 🙏.

@ranile
Copy link
Member Author

ranile commented Jul 23, 2021

I think this might hit on a common use case though - a Component's rendering might be conditional on only part of it's Properties value so PartialEq doesn't necessarily state whether a component should render, thus the need for changed. I think, however, in these cases, we are forcing users to still hold onto old prop values to compare with the new.

I agree, partially. There are cases where that's needed but there are also cases (see one of them, user might want to only make the call if the properties change) where it's needed outside of updated. If we were to provide this functionality, Context::last_prop which returns Option<&COMP::Properties> would be a better idea.

@mc1098
Copy link
Contributor

mc1098 commented Jul 23, 2021

where it's needed outside of updated.

Do you mean changed?

I'm not sure I follow your point with the example given, that seems to use the current props self.props which will be available using the Context passed to rendered.

If we were to provide this functionality, Context::last_prop which returns Option<&COMP::Properties> would be a better idea.

The only reason I think passing down the old props would be useful is because we seem to have it on hand in that update event and we can pass a reference1 to it when calling changed, after that it gets dropped from the scope and if it's the only Rc instance pointing to the memory then it's removed from the heap. If we hold onto it in the context then every Component pays the memory price for holding old props that they most likely will never want?

1 I don't mind if we pass the Rc down instead, but I'm not sure of a use case that tracks prop values that can't do this from accumulating from Context.

Part of my thinking is that if the change was made then you wouldn't need to hold children in the ContextProvider because changed would look like this:

fn changed(&mut self, old_props: &ContextProviderProps<T>, ctx: &Context<Self>) -> bool {
    let props = ctx.props();

    if self.context != props.context {
        self.context = props.context.clone();
        self.notify_consumers();
    }

    old_props.children != props.children
}

and children used in the view can just use ctx.props().children instead.

@ranile
Copy link
Member Author

ranile commented Jul 24, 2021

Do you mean changed?

Yes

I'm not sure I follow your point with the example given, that seems to use the current props self.props which will be available using the Context passed to rendered.

I wasn't clear enough, consider the following;

    fn rendered(&mut self, ctx: &Context<Self> first_render: bool) {
        let element = self.node_ref.cast::<TextField>().unwrap();
        if ctx.props().field_type != self.props.field_type {
            element.set_type(&JsValue::from(
                self.props.field_type.to_cow_string().as_ref(),
            ));
        }
        // ...

If there's ctx.last_props(), this use-case wouldn't require storing the props either.

Part of my thinking is that if the change was made then you wouldn't need to hold children in the ContextProvider because changed would look like this:

Agreed. But moving it inside Context would also cover the use-case I mentioned above.

@mc1098
Copy link
Contributor

mc1098 commented Jul 24, 2021

If there's ctx.last_props(), this use-case wouldn't require storing the props either.

I see thank you :)

Agreed. But moving it inside Context would also cover the use-case I mentioned above.

My proposal came at no cost - we have the old/last props available at the time of calling changed so we can happily pass a reference to it down.
The concern I have for having last_props in Context is that every Component has a copy of it's last_props sitting in memory even if it doesn't care about them (ofc only the case when the Component has had a Properties update event).
I'd say for the use case you showed, the user could clone the fields from props they wanted as Component state and opt-in to holding the memory required by their use case.

It might be that this use case is just going to be so common place that the convenience of having last_props outweighs any potential memory cost or it might be even with your clarification I'm missing a greater point here?

@mc1098
Copy link
Contributor

mc1098 commented Aug 2, 2021

I think this will close #1560 too :)

@mc1098 mc1098 mentioned this pull request Aug 5, 2021
3 tasks
/// Components handle messages in their `update` method and commonly use this method
/// to update their state and (optionally) re-render themselves.
fn update(&mut self, msg: Self::Message) -> ShouldRender;
fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> ShouldRender {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit:
When you use IDE auto completion for trait methods in an impl block, it will copy this exact line with the leading underscores in the argument names. I'd remove the underscores and add #[allow(unused_variables)] instead, so you don't have to remove them each time after auto completion.

Same for the other methods, of course.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, as this is a good opportunity, do you (and other contributors) think that ShouldRender should be kept as an alias? I don't see much benefit in it over a comment on what the returned bool means. Furthermore, it has been rather annoying to either import another symbol or replace it with a bool after auto completion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, as this is a good opportunity, do you (and other contributors) think that ShouldRender should be kept as an alias? I don't see much benefit in it over a comment on what the returned bool means.

I have no strong feelings either way. ShouldRender is a pretty nice alias, as alias go, but a comment on what the returned bool means seems good to me.

Furthermore, it has been rather annoying to either import another symbol or replace it with a bool after auto completion.

I guess I have gotten so used to flooding my namespace with yew::prelude::* to not have this issue :)

I'd vote to remove the alias then, as you (and others) probably hit this issue several times, whereas not knowing what the bool means in update and changed is generally only going to happen once and is easy to resolve (look at the comment).

@bakape
Copy link
Contributor

bakape commented Aug 12, 2021

My proposal came at no cost - we have the old/last props available at the time of calling changed so we can happily pass a reference to it down.
The concern I have for having last_props in Context is that every Component has a copy of it's last_props sitting in memory even if it doesn't care about them (ofc only the case when the Component has had a Properties update event).
I'd say for the use case you showed, the user could clone the fields from props they wanted as Component state and opt-in to holding the memory required by their use case.
It might be that this use case is just going to be so common place that the convenience of having last_props outweighs any potential memory cost or it might be even with your clarification I'm missing a greater point here?

I forgot to reply to this but:
Users who need to compare such props can hold on to those anyway. I'm not sure about the use cases of just having the last props available in changed. Maybe there's no point of doing that? Maybe that should be done? I'm not sure

This might be from my limited experience, but most times you don't care how the props changed and just want to rerender, if they did.

On the other hand, now that checking for prop change is done by the framework, hardly anyone would need to override changed() (instead of the previous change()), so you might as well add this extra perk as hardly anyone would suffer the slight increase in verbosity. As pointed out, it does also avoid extra boilerplate and memory for storing and updating the props inside the component itself. Passing the last props as an argument seems as a net positive overall.

@mc1098
Copy link
Contributor

mc1098 commented Aug 12, 2021

I forgot to reply to this but:
Users who need to compare such props can hold on to those anyway. I'm not sure about the use cases of just having the last props available in changed. Maybe there's no point of doing that? Maybe that should be done? I'm not sure

We could leave it as is, and see if a compelling use case comes up to justify this change?

It's pretty easy to add (when not tangled in an already large PR :D) and as @bakape said most users are unlikely to override changed so the breaking change would only effect a small proportion of users (not that that stops us anyways 😉)

Once Clippy is happy then this PR looks in good shape for merging 🎉

@ranile
Copy link
Member Author

ranile commented Aug 14, 2021

We could leave it as is, and see if a compelling use case comes up to justify this change?

I agree.

most users are unlikely to override changed so the breaking change would only effect a small proportion of users (not that that stops us anyways 😉)

While that is true, I think it would be a good idea to reduce the amount breaking changes in future releases.

Once Clippy is happy then this PR looks in good shape for merging tada

I didn't realize clippy was complaining. I'll fix that soon.

@mc1098
Copy link
Contributor

mc1098 commented Aug 14, 2021

most users are unlikely to override changed so the breaking change would only effect a small proportion of users (not that that stops us anyways 😉)

While that is true, I think it would be a good idea to reduce the amount breaking changes in future releases.

Mostly in jest :D but I think before 1.0.0 breaking changes should be expected and is worth it for correctness, ergonomics and performance improvements.

examples/webgl/src/main.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@mc1098 mc1098 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me 🎉

I think the bits about variable prefixes and the ShouldRender type can come in another PR; they are style choices and don't directly apply to these changes as they apply to Components V1 too.

Allows this PR to get merged in (if the maintainers are happy ofc), as it's big and conflict happy 🙃

Copy link
Contributor

@mc1098 mc1098 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge conflicts need to be resolved then I think we can push this over the finish line 🎉

ranile added 2 commits August 24, 2021 21:21
# Conflicts:
#	examples/boids/src/slider.rs
#	examples/crm/src/add_client.rs
#	examples/file_upload/src/main.rs
#	examples/js_callback/src/main.rs
#	examples/keyed_list/src/main.rs
#	examples/mount_point/src/main.rs
#	examples/store/src/text_input.rs
#	examples/todomvc/src/main.rs
#	packages/yew-macro/tests/derive_props/fail.stderr
@ranile
Copy link
Member Author

ranile commented Aug 24, 2021

The conflicts are resolved and CI is green

Copy link
Contributor

@mc1098 mc1098 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me 🚀

@siku2 siku2 merged commit f50c8c3 into yewstack:master Aug 25, 2021
WorldSEnder added a commit to futursolo/stylist-rs that referenced this pull request Aug 31, 2021
Not yet fully functional, due to yewdux not having upgraded to
yewstack/yew#1961. Specifically, some examples are not working,
but the stylist packages are fine.
@mc1098 mc1098 mentioned this pull request Sep 13, 2021
@voidpumpkin voidpumpkin added A-yew Area: The main yew crate A-yew-macro Area: The yew-macro crate labels Nov 26, 2021
Comment on lines +64 to +66
fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> ShouldRender {
false
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so unfortunate! 😅 @hamza1311 why did you put false here by default?

If you don't mind I will make a PR to change this to true for the next release. Is there any particular reasoning for why you used false here?

Putting this to true is very handy because it allows a basic component that only needs to refresh with a callback to refresh without having to define the fn update() method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-yew Area: The main yew crate A-yew-macro Area: The yew-macro crate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

An alternative to neq_assign_by Too much boilerplate for components
6 participants