Prevents Fallback UI from becoming suspended (#2532)

* Bring changes to this branch.

* Add BaseSuspense.

* Create detached parent inside of bundle.

* Fix ctx and opt for html!.

* Make Portals "work" with SSR.

* Update docs.

* Fix feature flags.

* Revert portal design.

* Fix feature flag.
This commit is contained in:
Kaede Hoshikawa 2022-03-20 17:01:40 +09:00 committed by GitHub
parent 9c2480b93b
commit b392023686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 127 additions and 99 deletions

View File

@ -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]
@ -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.
@ -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),
@ -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
@ -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,
);
@ -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,
);

View File

@ -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;
@ -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();
@ -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);

View File

@ -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.

View File

@ -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 {
@ -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};
impl Component for Suspense {
type Properties = SuspenseProps;
type Message = SuspenseMsg;
#[derive(Properties, PartialEq, Debug, Clone)]
pub(crate) struct BaseSuspenseProps {
pub children: Children,
fn create(ctx: &Context<Self>) -> Self {
Self {
link: ctx.link().clone(),
suspensions: Vec::new(),
#[cfg(target_arch = "wasm32")]
detached_parent: web_sys::window()
.and_then(|m| m.document())
.and_then(|m| m.create_element("div").ok()),
#[cfg(not(target_arch = "wasm32"))]
detached_parent: None,
}
pub fallback: Option<Html>,
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Self::Message::Suspend(m) => {
if m.resumed() {
return false;
#[derive(Debug)]
pub(crate) enum BaseSuspenseMsg {
Suspend(Suspension),
Resume(Suspension),
}
#[derive(Debug)]
pub(crate) struct BaseSuspense {
link: Scope<Self>,
suspensions: Vec<Suspension>,
}
impl Component for BaseSuspense {
type Properties = BaseSuspenseProps;
type Message = BaseSuspenseMsg;
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) => {
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));
self.suspensions.push(m);
true
}
Self::Message::Resume(ref m) => {
let suspensions_len = self.suspensions.len();
self.suspensions.retain(|n| m != n);
m.listen(self.link.callback(Self::Message::Resume));
self.suspensions.push(m);
true
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();
impl BaseSuspense {
pub(crate) fn suspend(&self, s: Suspension) {
self.link.send_message(BaseSuspenseMsg::Suspend(s));
}
let children_vnode =
VNode::from(VList::with_children(children.into_iter().collect(), None));
pub(crate) fn resume(&self, s: Suspension) {
self.link.send_message(BaseSuspenseMsg::Resume(s));
}
}
let vsuspense = VSuspense::new(
children_vnode,
fallback_vnode,
self.detached_parent.clone(),
!self.suspensions.is_empty(),
key,
);
/// 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();
VNode::from(vsuspense)
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()
}
}
#[cfg(not(any(feature = "ssr", feature = "csr")))]
pub use feat_no_csr_ssr::*;

View File

@ -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};

View File

@ -1,5 +1,4 @@
use super::{Key, VNode};
use web_sys::Element;
/// This struct represents a suspendable DOM fragment.
#[derive(Clone, Debug, PartialEq)]
@ -8,8 +7,6 @@ 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.
@ -17,17 +14,10 @@ pub struct VSuspense {
}
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,
}