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

Prevents Fallback UI from becoming suspended #2532

Merged
merged 10 commits into from Mar 20, 2022
19 changes: 8 additions & 11 deletions packages/yew/src/dom_bundle/bsuspense.rs
Expand Up @@ -4,6 +4,7 @@ use super::{BNode, Reconcilable, ReconcileTarget};
use crate::html::AnyScope;
use crate::virtual_dom::{Key, VSuspense};
use crate::NodeRef;
use gloo::utils::document;
use web_sys::Element;

/// The bundle implementation to [VSuspense]
Expand Down Expand Up @@ -56,11 +57,12 @@ impl Reconcilable for VSuspense {
let VSuspense {
children,
fallback,
detached_parent,
suspended,
key,
} = self;
let detached_parent = detached_parent.expect("no detached parent?");
let detached_parent = document()
.create_element("div")
.expect("failed to create detached element");

// When it's suspended, we render children into an element that is detached from the dom
// tree while rendering fallback UI into the original place where children resides in.
Expand Down Expand Up @@ -100,10 +102,7 @@ impl Reconcilable for VSuspense {
) -> NodeRef {
match bundle {
// We only preserve the child state if they are the same suspense.
BNode::Suspense(m)
if m.key == self.key
&& self.detached_parent.as_ref() == Some(&m.detached_parent) =>
{
BNode::Suspense(m) if m.key == self.key => {
self.reconcile(parent_scope, parent, next_sibling, m)
}
_ => self.replace(parent_scope, parent, next_sibling, bundle),
Expand All @@ -120,11 +119,9 @@ impl Reconcilable for VSuspense {
let VSuspense {
children,
fallback,
detached_parent,
suspended,
key: _,
} = self;
let detached_parent = detached_parent.expect("no detached parent?");

let children_bundle = &mut suspense.children_bundle;
// no need to update key & detached_parent
Expand All @@ -136,7 +133,7 @@ impl Reconcilable for VSuspense {
(true, Some(fallback_bundle)) => {
children.reconcile_node(
parent_scope,
&detached_parent,
&suspense.detached_parent,
NodeRef::default(),
children_bundle,
);
Expand All @@ -149,11 +146,11 @@ impl Reconcilable for VSuspense {
}
// Freshly suspended. Shift children into the detached parent, then add fallback to the DOM
(true, None) => {
children_bundle.shift(&detached_parent, NodeRef::default());
children_bundle.shift(&suspense.detached_parent, NodeRef::default());

children.reconcile_node(
parent_scope,
&detached_parent,
&suspense.detached_parent,
NodeRef::default(),
children_bundle,
);
Expand Down
6 changes: 3 additions & 3 deletions packages/yew/src/html/component/lifecycle.rs
Expand Up @@ -4,7 +4,7 @@ use super::scope::{AnyScope, Scope};
use super::BaseComponent;
use crate::html::{Html, RenderError};
use crate::scheduler::{self, Runnable, Shared};
use crate::suspense::{Suspense, Suspension};
use crate::suspense::{BaseSuspense, Suspension};
use crate::{Callback, Context, HtmlResult};
use std::any::Any;
use std::rc::Rc;
Expand Down Expand Up @@ -387,7 +387,7 @@ impl RenderRunner {
let comp_scope = state.inner.any_scope();

let suspense_scope = comp_scope
.find_parent_scope::<Suspense>()
.find_parent_scope::<BaseSuspense>()
.expect("To suspend rendering, a <Suspense /> component is required.");
let suspense = suspense_scope.get_component().unwrap();

Expand Down Expand Up @@ -419,7 +419,7 @@ impl RenderRunner {
if let Some(m) = state.suspension.take() {
let comp_scope = state.inner.any_scope();

let suspense_scope = comp_scope.find_parent_scope::<Suspense>().unwrap();
let suspense_scope = comp_scope.find_parent_scope::<BaseSuspense>().unwrap();
let suspense = suspense_scope.get_component().unwrap();

suspense.resume(m);
Expand Down
2 changes: 1 addition & 1 deletion packages/yew/src/html/component/scope.rs
Expand Up @@ -239,7 +239,7 @@ mod feat_ssr {
}

#[cfg(not(any(feature = "ssr", feature = "csr")))]
mod feat_no_render_ssr {
mod feat_no_csr_ssr {
use super::*;

// Skeleton code to provide public methods when no renderer are enabled.
Expand Down
185 changes: 112 additions & 73 deletions packages/yew/src/suspense/component.rs
@@ -1,9 +1,4 @@
use crate::html::{Children, Component, Context, Html, Properties, Scope};
use crate::virtual_dom::{Key, VList, VNode, VSuspense};

use web_sys::Element;

use super::Suspension;
use crate::html::{Children, Html, Properties};

#[derive(Properties, PartialEq, Debug, Clone)]
pub struct SuspenseProps {
Expand All @@ -12,95 +7,139 @@ pub struct SuspenseProps {

#[prop_or_default]
pub fallback: Html,

#[prop_or_default]
pub key: Option<Key>,
}

#[derive(Debug)]
pub enum SuspenseMsg {
Suspend(Suspension),
Resume(Suspension),
}
#[cfg(any(feature = "csr", feature = "ssr"))]
mod feat_csr_ssr {
use super::*;

/// Suspend rendering and show a fallback UI until the underlying task completes.
#[derive(Debug)]
pub struct Suspense {
link: Scope<Self>,
suspensions: Vec<Suspension>,
detached_parent: Option<Element>,
}
use crate::html::{Children, Component, Context, Html, Scope};
use crate::suspense::Suspension;
use crate::virtual_dom::{VNode, VSuspense};
use crate::{function_component, html};

#[derive(Properties, PartialEq, Debug, Clone)]
pub(crate) struct BaseSuspenseProps {
pub children: Children,

impl Component for Suspense {
type Properties = SuspenseProps;
type Message = SuspenseMsg;
pub fallback: Option<Html>,
}

#[derive(Debug)]
pub(crate) enum BaseSuspenseMsg {
Suspend(Suspension),
Resume(Suspension),
}

fn create(ctx: &Context<Self>) -> Self {
Self {
link: ctx.link().clone(),
suspensions: Vec::new(),
#[derive(Debug)]
pub(crate) struct BaseSuspense {
link: Scope<Self>,
suspensions: Vec<Suspension>,
}

#[cfg(target_arch = "wasm32")]
detached_parent: web_sys::window()
.and_then(|m| m.document())
.and_then(|m| m.create_element("div").ok()),
impl Component for BaseSuspense {
type Properties = BaseSuspenseProps;
type Message = BaseSuspenseMsg;

#[cfg(not(target_arch = "wasm32"))]
detached_parent: None,
fn create(ctx: &Context<Self>) -> Self {
Self {
link: ctx.link().clone(),
suspensions: Vec::new(),
}
}
}

fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Self::Message::Suspend(m) => {
if m.resumed() {
return false;
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Self::Message::Suspend(m) => {
assert!(
ctx.props().fallback.is_some(),
"You cannot suspend from a component rendered as a fallback."
);

if m.resumed() {
return false;
}

m.listen(self.link.callback(Self::Message::Resume));
m.listen(self.link.callback(Self::Message::Resume));

self.suspensions.push(m);
self.suspensions.push(m);

true
true
}
Self::Message::Resume(ref m) => {
let suspensions_len = self.suspensions.len();
self.suspensions.retain(|n| m != n);

suspensions_len != self.suspensions.len()
}
}
Self::Message::Resume(ref m) => {
let suspensions_len = self.suspensions.len();
self.suspensions.retain(|n| m != n);
}

suspensions_len != self.suspensions.len()
fn view(&self, ctx: &Context<Self>) -> Html {
let BaseSuspenseProps { children, fallback } = (*ctx.props()).clone();
let children = html! {<>{children}</>};

match fallback {
Some(fallback) => {
let vsuspense = VSuspense::new(
children,
fallback,
!self.suspensions.is_empty(),
// We don't need to key this as the key will be applied to the component.
None,
);

VNode::from(vsuspense)
}
None => children,
}
}
}

fn view(&self, ctx: &Context<Self>) -> Html {
let SuspenseProps {
children,
fallback: fallback_vnode,
key,
} = (*ctx.props()).clone();

let children_vnode =
VNode::from(VList::with_children(children.into_iter().collect(), None));

let vsuspense = VSuspense::new(
children_vnode,
fallback_vnode,
self.detached_parent.clone(),
!self.suspensions.is_empty(),
key,
);

VNode::from(vsuspense)
impl BaseSuspense {
pub(crate) fn suspend(&self, s: Suspension) {
self.link.send_message(BaseSuspenseMsg::Suspend(s));
}

pub(crate) fn resume(&self, s: Suspension) {
self.link.send_message(BaseSuspenseMsg::Resume(s));
}
}

/// Suspend rendering and show a fallback UI until the underlying task completes.
#[function_component]
pub fn Suspense(props: &SuspenseProps) -> Html {
let SuspenseProps { children, fallback } = props.clone();

let fallback = html! {
<BaseSuspense fallback={None}>
{fallback}
</BaseSuspense>
};

html! {
<BaseSuspense {fallback}>
{children}
</BaseSuspense>
}
}
}

#[cfg(any(feature = "csr", feature = "ssr"))]
impl Suspense {
pub(crate) fn suspend(&self, s: Suspension) {
self.link.send_message(SuspenseMsg::Suspend(s));
}
pub use feat_csr_ssr::*;

pub(crate) fn resume(&self, s: Suspension) {
self.link.send_message(SuspenseMsg::Resume(s));
#[cfg(not(any(feature = "ssr", feature = "csr")))]
mod feat_no_csr_ssr {
use super::*;

use crate::function_component;

/// Suspend rendering and show a fallback UI until the underlying task completes.
#[function_component]
pub fn Suspense(_props: &SuspenseProps) -> Html {
Html::default()
Comment on lines +138 to +140
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 here to be replaced when SSR hydration is a thing, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a skeleton code for libraries when they don't have any renderer enabled (which means that they never gets rendered).

If we apply feature flags on the actual Suspense component multiple feature flags will be needed.

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 in the long term, it may be beneficial to separate rendering logic into a separate crate so we don't keep adding feature flags.

}
}

#[cfg(not(any(feature = "ssr", feature = "csr")))]
pub use feat_no_csr_ssr::*;
2 changes: 2 additions & 0 deletions packages/yew/src/suspense/mod.rs
Expand Up @@ -3,5 +3,7 @@
mod component;
mod suspension;

#[cfg(any(feature = "csr", feature = "ssr"))]
pub(crate) use component::BaseSuspense;
pub use component::Suspense;
pub use suspension::{Suspension, SuspensionHandle, SuspensionResult};
12 changes: 1 addition & 11 deletions packages/yew/src/virtual_dom/vsuspense.rs
@@ -1,5 +1,4 @@
use super::{Key, VNode};
use web_sys::Element;

/// This struct represents a suspendable DOM fragment.
#[derive(Clone, Debug, PartialEq)]
Expand All @@ -8,26 +7,17 @@ pub struct VSuspense {
pub(crate) children: Box<VNode>,
/// Fallback nodes when suspended.
pub(crate) fallback: Box<VNode>,
/// The element to attach to when children is not attached to DOM
pub(crate) detached_parent: Option<Element>,
/// Whether the current status is suspended.
pub(crate) suspended: bool,
/// The Key.
pub(crate) key: Option<Key>,
}

impl VSuspense {
pub(crate) fn new(
children: VNode,
fallback: VNode,
detached_parent: Option<Element>,
suspended: bool,
key: Option<Key>,
) -> Self {
pub fn new(children: VNode, fallback: VNode, suspended: bool, key: Option<Key>) -> Self {
Self {
children: children.into(),
fallback: fallback.into(),
detached_parent,
suspended,
key,
}
Expand Down