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

Suspense Support #2212

Merged
merged 35 commits into from Jan 5, 2022
Merged

Suspense Support #2212

merged 35 commits into from Jan 5, 2022

Conversation

futursolo
Copy link
Member

@futursolo futursolo commented Nov 28, 2021

Description

This pull request introduces an experimental Suspense support.

Summary:

  • Added HtmlResult as an alias to RenderResult<Html>.
  • A RenderError is added with Suspended being the only variant at the moment.
  • A <Suspense /> component is added that renders a fallback UI when receiving a Suspension.
  • A Suspension and SuspensionHandle type to suspend component rendering.
  • A VSuspense variant on virtual dom to facilitate special rendering behaviour (preserves component state when suspended).

Checklist

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

@futursolo
Copy link
Member Author

futursolo commented Nov 28, 2021

I imagine that this needs a lot of tests. So I submitted the PR as I add tests so anyone who wants to give it a shot can have a try and see if the design have flaws / can be improved.

Mainly now Html is a result, so it will break things. But none of the examples broke badly and are quick to fix.

@github-actions
Copy link

github-actions bot commented Nov 28, 2021

Visit the preview URL for this PR (updated for commit 5a376af):

https://yew-rs--pr2212-suspense-7k6vaamf.web.app

(expires Wed, 12 Jan 2022 11:05:27 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@WorldSEnder
Copy link
Member

If there's a VSuspense variant on VNode, why is Err(RenderError::Suspended) even needed? As far as I see, this is never even instantiated, only matched against.

@futursolo
Copy link
Member Author

If there's a VSuspense variant on VNode, why is Err(RenderError::Suspended) even needed? As far as I see, this is never even instantiated, only matched against.

These 2 are used for different purposes.

VSuspense is used by <Suspense /> component to switch between fallback and children where as RenderError::Suspended is returned by the component that actually suspends.

RenderError::Suspended is almost never constructed but includes a from implementation of Suspension.

A hook that suspends a component returns a Err(Suspension) when it wants to suspend rendering of the component and it will be converted to Err(RenderError::Suspended) by ?.

@futursolo
Copy link
Member Author

futursolo commented Nov 30, 2021

I have added tests and docs to this PR and now it's ready for review.

@voidpumpkin
Copy link
Member

Sorry this PR will probably be a hostage until we actually release 0.19

@hamza1311
Copy link
Member

I really don't like this change

Html is now aliased to RenderResult<VNode>.

This means that every view method/function component now needs to return an Ok(_). The html! macro should not return a Result since the generated code isn't fallible. We need to look into other ways of implementing this functionality before merging this in.

As a side note, can we not call it "Suspense"? It seems like everything in Yew is ripped off from React and even in React, Suspense name isn't a clear representation of what it does.

@futursolo
Copy link
Member Author

I really don't like this change

Html is now aliased to RenderResult<VNode>.

This means that every view method/function component now needs to return an Ok(_). The html! macro should not return a Result since the generated code isn't fallible. We need to look into other ways of implementing this functionality before merging this in.

I also wish there's a better way to do this. However, Suspense requires an early interruption of the function component rendering flow. This is done via throw in JavaScript. In Rust, there're 2 ways that this could be done:

  1. Result with ?
  2. Macros

There's no fundamental difference between these 2 approaches as ? is simply a macro (used to be try! macro) that expands to Err(e) => return Err(e). For the macro-based approach, all hooks that suspends needs to be a macro (e.g.:use_query!(), which internally try!'s automatically).

As for type Html = RenderResult<VNode>; & html!, there're a couple ways to implement this:

A:

type Html = RenderResult<VNode>;

#[function_component(Comp)]
fn comp() -> Html {
    html! { ... }
}

B:

type HtmlResult = RenderResult<Html>;

#[function_component(Comp)]
fn comp() -> HtmlResult {
    Ok(html! { ... })
}

C:

// Magic in Procedural Macro, Suspends with custom syntax.
#[function_component(Comp)]
fn comp() -> Html {
    html! { ... }
}

After evaluating the design, I think B's design is the most "correct" design but likely to cause difficulty in migration.
As for C, I cannot come up with something better than ? as the suspending operator.

Hence, I have picked A.

As a bonus point, I was able to fixed all examples with very little changes (in ~5 mins).

As a side note, can we not call it "Suspense"? It seems like everything in Yew is ripped off from React and even in React, Suspense name isn't a clear representation of what it does.

I think suspense naming is appropriate here.

<Suspense /> naming is also used by Vue (and Preact) and already widely accepted by React & Vue developers.

One point that should be taken into consideration is that:
Suspense is not a single feature, it's the core of an entire set of features
(SuspenseList, useTransition, useDeferredValue, and more).
I plan to gradually implementing those for Yew as well if this PR is accepted in some form.
If we rename Suspense, should we do the same to other features as well?

In addition, none of these features are straightforward to understand.
If we don't use the same name as React, we are likely going to have a note in the doc:
"Yew's <LoadingPlaceholder /> functions like React's <Suspense />".

Which kind of achieves the same effect of saying that this is "inspired" by React.

@voidpumpkin voidpumpkin added A-yew Area: The main yew crate A-yew-macro Area: The yew-macro crate labels Dec 15, 2021
@hamza1311
Copy link
Member

Instead of returning Result<VNode>, we could do the following:

struct VSuspense {
    fallback: VNode,
    content: VNode,
}

enum VNode {
    // ...
    VSuspense(VSuspense)
}

// in Suspense component

#[derive(Properties, PartialEq)]
struct SusProps {
    fallback: Html,
    children: Html,
}

#[function_component(Suspense)]
fn sus(props: &SusProps) -> Html {
    VNode::VSuspense(VSuspense {
        fallback: props.fallback.clone(),
        content: props.children.clone()
    })
}


// somewhere else

let fallback = html! { <Fallback /> };

html! {
     <Suspense {fallback}>
         <div>content</div>
     </Suspense>

We could also implement the Try trait so (on nightly) ? can also be used.

@futursolo
Copy link
Member Author

Instead of returning Result<VNode>, we could do the following:

struct VSuspense {
    fallback: VNode,
    content: VNode,
}

enum VNode {
    // ...
    VSuspense(VSuspense)
}

// in Suspense component

#[derive(Properties, PartialEq)]
struct SusProps {
    fallback: Html,
    children: Html,
}

#[function_component(Suspense)]
fn sus(props: &SusProps) -> Html {
    VNode::VSuspense(VSuspense {
        fallback: props.fallback.clone(),
        content: props.children.clone()
    })
}


// somewhere else

let fallback = html! { <Fallback /> };

html! {
     <Suspense {fallback}>
         <div>content</div>
     </Suspense>

We could also implement the Try trait so (on nightly) ? can also be used.

The current implementation already has a VSuspense variant on VNode. And the <Suspense /> component is not the reason why we need to make Html = RenderResult<VNode>.

See: https://github.com/yewstack/yew/pull/2212/files#diff-0cd3c02c95dd9d4c5a156d6ac41322a90b238aa47a686cb052e35da7f13c7112R79

The place that actually needs Err(RenderError::Suspended(_)) is the component that is currently suspended.

See: https://github.com/yewstack/yew/pull/2212/files#diff-69584225471214af964defca7edc8911d478edfc3608e8f38faf17d17b23121bR17

Please see my previous comment about why VSuspense is not a replacement for RenderResult<VNode>: #2212 (comment)

@voidpumpkin
Copy link
Member

voidpumpkin commented Dec 17, 2021

I have mixed views on this feature. And I am writing this while i looked only to the modified examples code, so pardon me if some comments are not well-informed.

Pros

It will let us write less boilerplate in prod code that is like this:

let bob = match fetched_bob {
    Some(some) => some,
    None => return html!{<MyFallback />},
}

Though to be honest the upper thing could be solved simply by getting let else statement stable https://rust-lang.github.io/rfcs/3137-let-else.html

Other than that I don't see the benefit of specifying the fallback on the parent.

Though can the suspense cascade up the tree more than once?
If the answer is yes i think that's a big pro as well, to have one fallback for all the child component tree.

Cons?

I ended up rambling in cons so you can tldr it at the end
The dark side of this change is that people might end up implementing custom into conversions from all errors to this unified error that gets returned in the view function.

While inherently its not a bad thing, and definitely it will lessen the amount of boilerplate error handling, I think it a little drives away from the rust model where all errors should be explicitly handled.
It almost feels like suddenly we have throw functionality in yew.

But then again it kind of still makes sense because its like collecting all the errors at the root DOM done and not rendering anything faulty.

TLDR: I see only pros with the feature idea. I don't see any cons that actually forces anyone into a bad scenario.

@voidpumpkin
Copy link
Member

Regarding the A, B and C options.
I would also go for B as it properly states we are returning a Result there.

Copy link
Member

@voidpumpkin voidpumpkin left a comment

Choose a reason for hiding this comment

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

I went through the PR, now i realize i was mixing up Suspense with error boundaries a little, sorry for that.
I am rejecting until proper steps are made towards A, B or C options, depending on which one we decide to use in this PR discussion.

@futursolo
Copy link
Member Author

After some consideration, I have decided not to choose Option D.

For the following reasons:

  1. I have chosen not to include struct component because it's likely that people are going to write code that are less than ideal and it's hard to explain why you shouldn't do that. (See: Suspense Support #2212 (comment)) Limiting suspension to hooks mitigates this issue greatly as you can only have a fixed number of hooks in a component.
  2. One of the primary reasons of type Html = RenderResult<VNode> is that it would require a rewrite of every component for no good reason and having to do type Render = Html for every component sort of achieves the same effect (annoyance?).
  3. The change set is unnecessarily big for one feature (requires a rewrite of every component, which is also the original reason why Option B is not selected).

Copy link
Member

@hamza1311 hamza1311 left a comment

Choose a reason for hiding this comment

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

There's files committed (shown in the diff) but without any changes. Can you fix that?

Comment on lines 215 to 217
// When a component ends with Internal, it will become possessive.
// InternalsInternal instead of InternalInternal
provider_name_str.push_str("sInternal");
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't cover the following case: InternalsInternal. That will become InternalssInternal. Besides, why even bother with the provider name? It's not like it really matters.

Copy link
Member Author

Choose a reason for hiding this comment

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

InternalsInternal's Internal will become InternalsInternalsInternal.

examples/suspense/Cargo.toml Outdated Show resolved Hide resolved
examples/suspense/src/use_sleep.rs Outdated Show resolved Hide resolved
Comment on lines +61 to +69
// no longer possible?
// #[::yew::function_component(ConstGenerics)]
// fn const_generics<const N: ::std::primitive::i32>() -> ::yew::Html {
// ::yew::html! {
// <div>
// { N }
// </div>
// }
// }
Copy link
Member

Choose a reason for hiding this comment

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

What happened here? Seems like this PR broke something since the CI passes on master.

Copy link
Member Author

@futursolo futursolo Dec 20, 2021

Choose a reason for hiding this comment

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

const generics are not possible in functions and the updated implementation now uses a function.

packages/yew/src/html/component/mod.rs Outdated Show resolved Hide resolved
packages/yew/src/suspense/suspension.rs Show resolved Hide resolved
hook should return a `Err(Suspension)` and users should unwrap it with
`?` in which it will be converted into `Html`.

```rust ,ignore
Copy link
Member

Choose a reason for hiding this comment

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

Can we not ignore this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not ignoring it would require including a complete hook implementation on every snippet.

I have included a complete example below.

Copy link
Member

Choose a reason for hiding this comment

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

I wonder how long will it take for docusaurus to finally merge this in facebook/docusaurus#5783
Then we could include all needed code and just hide it.

can use a function component as a Higher-Order-Component to
achieve suspense-based data fetching.

```rust, ignore
Copy link
Member

Choose a reason for hiding this comment

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

Again, let's not ignore here the snippets

voidpumpkin
voidpumpkin previously approved these changes Dec 26, 2021
Copy link
Member

@voidpumpkin voidpumpkin left a comment

Choose a reason for hiding this comment

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

Well looks fine to me, though i fully admit that due to me not contributing that much code wise i can miss things.

# Conflicts:
#	packages/yew/src/html/component/scope.rs
#	packages/yew/src/lib.rs
#	website/docs/concepts/function-components/custom-hooks.mdx
#	website/docs/concepts/function-components/pre-defined-hooks.mdx
@voidpumpkin
Copy link
Member

voidpumpkin commented Dec 30, 2021

I wonder if suspense is a good feature to have considering you could reproduce this error very easily as using a ? essentially is an early return

(but i need to test this)

@futursolo
Copy link
Member Author

futursolo commented Dec 30, 2021

I wonder if suspense is a good feature to have considering you could reproduce this error very easily as using a ? essentially is an early return

(but i need to test this)

There's a test covering early return, and it's not reproducible in the test case.

https://github.com/yewstack/yew/pull/2212/files#diff-a17faa4af94b27d70b057bc6bb4a8b12df9e0eb35597a0c5ac9c9fa26bc689f1R203

I think the issue you are experiencing is something different. Because the number of hooks is not limited at the moment.
If it is limited after first call, you should get a different error (https://github.com/yewstack/yew/blob/master/packages/yew/src/functional/hooks/mod.rs#L58).

The error mentioned in the discord (where hooks fail to downcast): https://github.com/yewstack/yew/blob/master/packages/yew/src/functional/hooks/mod.rs#L72

# Conflicts:
#	packages/yew/tests/use_state.rs
Copy link
Member

@hamza1311 hamza1311 left a comment

Choose a reason for hiding this comment

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

LGTM

I will admit, I didn't inspect literally everything but it this should be good. If something comes up, it can be fixed later on.

@hamza1311 hamza1311 merged commit ac3af0a into yewstack:master Jan 5, 2022
Comment on lines +502 to +507
/// Move elements from one parent to another parent.
/// This is currently only used by `VSuspense` to preserve component state without detaching
/// (which destroys component state).
/// Prefer `detach` then apply if possible.
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef);

Copy link
Member

Choose a reason for hiding this comment

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

Can you comment why this can't reuse VNode::move_before? There is a slight difference in that the next_sibling is a NodeRef instead of the dereferenced Option<&Node> but I don't immediately see how that matters.

Copy link
Member Author

Choose a reason for hiding this comment

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

move_before assumes previous position and next position are under the same parent and hence does not attempt to update ComponentState where as shift has a different parent and hence is updated via push_component_update for VComp.

@futursolo futursolo deleted the suspense branch January 27, 2022 13:58
da-x pushed a commit to da-x/yew that referenced this pull request Jul 27, 2022
* Make Html a Result.

* Fix tests.

* Implement Suspense.

* Schedule render when suspension is resumed.

* Shift children into a detached node.

* styled example.

* Update wording a little bit.

* Move hint to hint.

* Add some tests.

* Fix clippy.

* Add docs.

* Add to sidebar.

* Fix syntax highlight.

* Component -> BaseComponent.

* Html -> VNode, HtmlResult = RenderResult<Html>.

* Suspendible Function Component.

* Add a method to create suspension from futures.

* Revert extra changes.

* Fix tests.

* Update documentation.

* Switch to custom trait to make test reliable.

* Fix file permission.

* Fix docs.

* Remove log.

* Fix file permission.

* Fix component name error.

* Make Suspension a future.
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 breaking change
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants