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

Implement imperative refs, capturing scopes to child components #2551

Closed
wants to merge 43 commits into from

Conversation

WorldSEnder
Copy link
Member

@WorldSEnder WorldSEnder commented Mar 26, 2022

Description

Instead of getting a ref "the first element", now get a link to the component the ref is attached to.
This is a breaking change. Components wanting to preserve the old behavior should add a property and forward that.
There's probably a bit of documentation to adjust, I haven't check all places yet.

Fixes #2505
Fixes #1257
Fixes #905
Fixes #2197

Related #905

Checklist

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

github-actions[bot]
github-actions bot previously approved these changes Mar 26, 2022
@WorldSEnder WorldSEnder added feature-request A feature request breaking change A-examples Area: The examples A-yew Area: The main yew crate A-yew-macro Area: The yew-macro crate labels Mar 26, 2022
@github-actions
Copy link

github-actions bot commented Mar 26, 2022

Visit the preview URL for this PR (updated for commit 3eeae19):

https://yew-rs--pr2551-imperative-ref-gvsnyl7h.web.app

(expires Sun, 22 May 2022 23:35:53 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link

github-actions bot commented Mar 26, 2022

Size Comparison

examples master (KB) pull request (KB) diff (KB) diff (%)
boids 172.763 175.102 +2.339 +1.354%
contexts 109.638 112.767 +3.129 +2.854%
counter 86.651 87.856 +1.205 +1.391%
counter_functional 87.301 88.714 +1.413 +1.619%
dyn_create_destroy_apps 89.799 91.477 +1.678 +1.868%
file_upload 102.619 103.973 +1.354 +1.319%
function_memory_game 167.338 171.123 +3.785 +2.262%
function_router 350.798 358.047 +7.249 +2.066%
function_todomvc 161.984 165.024 +3.040 +1.877%
futures 226.613 227.979 +1.366 +0.603%
game_of_life 107.539 108.677 +1.138 +1.058%
inner_html 83.688 84.452 +0.765 +0.914%
js_callback 112.864 116.021 +3.156 +2.797%
keyed_list 195.036 197.017 +1.980 +1.015%
light_switch N/A 101.887 N/A N/A
mount_point 86.281 87.588 +1.307 +1.514%
nested_list 115.914 118.623 +2.709 +2.337%
node_refs 90.448 92.388 +1.939 +2.144%
password_strength 1539.540 1541.569 +2.029 +0.132%
portals 97.192 99.455 +2.263 +2.328%
router 319.843 323.520 +3.677 +1.150%
simple_ssr 494.012 497.199 +3.188 +0.645%
ssr_router 425.553 433.719 +8.166 +1.919%
suspense 110.582 113.617 +3.035 +2.745%
timer 89.360 90.550 +1.189 +1.331%
todomvc 143.053 144.208 +1.155 +0.808%
two_apps 87.286 88.486 +1.200 +1.375%
web_worker_fib 153.319 154.835 +1.516 +0.989%
webgl 87.373 88.424 +1.051 +1.203%

⚠️ The following examples have changed their size significantly:

examples master (KB) pull request (KB) diff (KB) diff (%)
boids 172.763 175.102 +2.339 +1.354%
contexts 109.638 112.767 +3.129 +2.854%
counter 86.651 87.856 +1.205 +1.391%
counter_functional 87.301 88.714 +1.413 +1.619%
dyn_create_destroy_apps 89.799 91.477 +1.678 +1.868%
file_upload 102.619 103.973 +1.354 +1.319%
function_memory_game 167.338 171.123 +3.785 +2.262%
function_router 350.798 358.047 +7.249 +2.066%
function_todomvc 161.984 165.024 +3.040 +1.877%
game_of_life 107.539 108.677 +1.138 +1.058%
js_callback 112.864 116.021 +3.156 +2.797%
keyed_list 195.036 197.017 +1.980 +1.015%
mount_point 86.281 87.588 +1.307 +1.514%
nested_list 115.914 118.623 +2.709 +2.337%
node_refs 90.448 92.388 +1.939 +2.144%
portals 97.192 99.455 +2.263 +2.328%
router 319.843 323.520 +3.677 +1.150%
ssr_router 425.553 433.719 +8.166 +1.919%
suspense 110.582 113.617 +3.035 +2.745%
timer 89.360 90.550 +1.189 +1.331%
two_apps 87.286 88.486 +1.200 +1.375%
webgl 87.373 88.424 +1.051 +1.203%

github-actions[bot]
github-actions bot previously approved these changes Mar 26, 2022
github-actions[bot]
github-actions bot previously approved these changes Mar 26, 2022
github-actions[bot]
github-actions bot previously approved these changes Mar 26, 2022
github-actions[bot]
github-actions bot previously approved these changes Mar 26, 2022
github-actions[bot]
github-actions bot previously approved these changes Mar 26, 2022
@WorldSEnder
Copy link
Member Author

WorldSEnder commented Mar 26, 2022

I don't seem to know what's causing the code-size bloat, in some cases. If anyone can enlighten me, feel free to let me know. EDIT: fixed, for the most part. <2kb for the feature now across the board.

@ranile
Copy link
Member

ranile commented Mar 26, 2022

I don't seem to know what's causing the code-size bloat, in some cases. If anyone can enlighten me, feel free to let me know.

You can use twiggy to compare binary generated from master and your branch

github-actions[bot]
github-actions bot previously approved these changes Mar 27, 2022
github-actions[bot]
github-actions bot previously approved these changes Mar 27, 2022
github-actions[bot]
github-actions bot previously approved these changes May 15, 2022
github-actions[bot]
github-actions bot previously approved these changes May 15, 2022
this should shave some size from BaseComponent::create
github-actions[bot]
github-actions bot previously approved these changes May 15, 2022
///
/// We provide a blanket implementation of this trait for every member that implements
/// [`Component`].
pub trait ComponentWithRef: Sized + 'static {
Copy link
Member

Choose a reason for hiding this comment

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

Does this exist only to avoid the breaking change of adding type Reference to component? If so, I'd be fine with making that breaking change.

We can add a nightly feature flag that avoids the breaking change in nightly builds using default value for Reference.

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 think we can use Component as ComponentWithRef; with the nightly flag. Can we discuss this in a separate PR?

github-actions[bot]
github-actions bot previously approved these changes May 15, 2022
github-actions[bot]
github-actions bot previously approved these changes May 15, 2022
@WorldSEnder
Copy link
Member Author

WorldSEnder commented May 15, 2022

To summarize the current state:

HtmlRef on elements are still not typed. I have an almost working solution that integrates with yew-macro, but the following problem:

The Html*Elements are behind features flags in web_sys and the types are only usable when these are turned on. Roughly, the problem is that the element type appears in a constraint bound.

Solution 1 would be to enable all relevant flags in the dependency for Yew, which will lead to probably longer build times for anybody not using them (but shouldn't impact code size). Solution 2 would be to somehow detect which flags are turned on and fall back to only allowing HtmlRef<Element> for those (if the feature isn't enabled, the user can't provide a more precise type anyway).

use_imperative_handle is left for a follow up PR. I believe it's not critical functionality and should work regardless.

Regarding the bundle size, the increase is mainly due to maintaining both the DomPosition and NodeRef, which was previously the same but are now separate. The result being that some lifecycle methods and Runners have grown slightly. That is (per component, roughly, can vary a lot) Scope::mount +200B, CompState::new +300B, PropsUpdate +300B. Feel free to investigate yourself, but it's not directly caused by HtmlRef, rather an artifact of separating internal DomPosition from HtmlRef.

@WorldSEnder WorldSEnder requested a review from ranile May 15, 2022 23:46
Copy link
Member

@futursolo futursolo left a comment

Choose a reason for hiding this comment

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

That is (per component, roughly, can vary a lot) Scope::mount +200B, CompState::new +300B, PropsUpdate +300B. Feel free to investigate yourself, but it's not directly caused by HtmlRef, rather an artifact of separating internal DomPosition from HtmlRef.

#2567 also separates HtmlRef and NodeRef. I don't think separating these 2 types is the culprit of the code size increase.

The code size increases on the path where ref is passed, which should be due to how the ref is passed and the inability to eliminate these code when Reference = NoReference.

(The code size can also be impacted by how many fields present on a struct.)

/// [`ComponentWithRef::create`]: crate::html::ComponentWithRef::create
#[derive(Debug)]
pub struct BindableRef<'b, T> {
inner: &'b ErasedHtmlRef,
Copy link
Member

Choose a reason for hiding this comment

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

Does this mean that the ref must be set in create() due to 'b?

If so, I think there needs to be a way to delay this until rendered().

  • function components do not have a way to access create(), the earliest possible render cycle is render().
  • ref can be used to update component states which can interfere hydration if done so before rendered which means that use_imperative_handle should delay setting ref until effects.

Copy link
Member Author

Choose a reason for hiding this comment

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

You'd use bindable_ref.foward() to obtain a HtmlRef to bind to an html element during render. I agree that it the binding should perhaps only "take effect" after rendered, but it'd complicate the implementation. As long as we don't have callback refs, just be careful. Refs to Nodes are also only accessible after rendered, so this shouldn't be a surprise to users. In any case, function component can use internal api to set the HtmllRef later

type Message = Msg;
type Properties = Props;
type Reference = Scope<Self>;
Copy link
Member

Choose a reason for hiding this comment

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

This does not quite solve the question I mentioned.

Suppose there is a component with 2 refs, input_ref and ref.

#[derive(Properties, PartialEq)]
pub struct MyInputProps {
    input_ref: HtmlRef<Element>,
}

// Suppose there is a third-party UI library
// Using custom component as an example, 
// but applies to any UI library that cannot be created by directly rendering layout.
// e.g.: React, etc.
let el = document().create_element("my-input");
let input_el = el.query_selector("input");

let html = VNode::VRef(el);

bindable_ref.set(el);
// how to set input_ref to input_el?


/// Use this type as the [`BaseComponent::Reference`] type when a component can not be referenced.
#[derive(Debug, Clone)]
pub enum NoReference {}
Copy link
Member

Choose a reason for hiding this comment

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

Is there any reason of making a NoReference type than using () here?

Setting ref type to () and expecting it to not be set (in debug_assert_bound) also makes sense to me.

Copy link
Member Author

@WorldSEnder WorldSEnder May 22, 2022

Choose a reason for hiding this comment

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

An empty enum makes more sense to me. This is intended to be publicly visible. That is, you should be able to match:

assert!(no_ref.get().is_none()); // not bound. In fact:
match no_ref.get() {
    None => {}, // not bound
    Some(what) => match what {}, // exactly no variants to match, can safely ignore this branch
}

@WorldSEnder
Copy link
Member Author

WorldSEnder commented May 23, 2022

The code size increases on the path where ref is passed, which should be due to how the ref is passed and the inability to eliminate these code when Reference = NoReference.

The only way not requiring user interaction I can currently see, that works, is using either the #[feature(specialization)] or maybe #[feature(const_type_id)]. Both of which are available on nightly only. One has to teach rust that a generic argument is different from NoReference, basically. How bad would it be if you had to add an (empty) implementation on each type to use as a Ref, I don't think too bad.

And please don't compare it so shallowly with #2567, which contains no code for references on components, hence only has to deal with the two types on *tag, but not on *comp nor in lifecycles.

@futursolo
Copy link
Member

And please don't compare it so shallowly with #2567, which contains no code for references on components, hence only has to deal with the two types on *tag, but not on *comp nor in lifecycles.

I am not shallowly comparing this pull request with #2567.
#2567 has many other favourable properties that is missing from this pull request.
(e.g.: It does not use any downcasting.)

I realise that these might not be possible with this approach and is happy to overlook them.

However, If the code size is not solvable, then it's a limitation of this current design that should be factored in when evaluating this pull request.

This pull request is trying to enforce situations that does not always hold and that has resulted in significant complexity in the design to provide escape hatches.

  1. Each component as 1 ref type associated.
    A component can have 2 or more refs.
    However, any additional refs cannot be associated like ref and are not expressed on the Component type.
  2. ref must be set inside the bound component and at create() cycle.
    It would need a private API to set it for function components in render() or rendered() cycle.
    It also requires a forward() method to "downgrade" this back to HtmlRef before forwarding to another component / element.
    It's not the always case to say that additional refs can be forwarded as a prop to a VNode-like tag as mentioned in comment above.
  3. ref must be set by rendered().
    It can be forwarded into a component that is suspended at first render and hence wouldn't be ready by rendered.
    There needs to be a way to opt-out this check.

@WorldSEnder WorldSEnder mentioned this pull request Jun 26, 2022
29 tasks
@ranile ranile mentioned this pull request Jul 19, 2022
2 tasks
@WorldSEnder
Copy link
Member Author

Closing this for now, since #2783 has been merged. A future PR can introduce the component ref.

@WorldSEnder WorldSEnder closed this Aug 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-examples Area: The examples A-yew Area: The main yew crate A-yew-macro Area: The yew-macro crate breaking change feature-request A feature request
Projects
None yet
3 participants