mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
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:
parent
9c2480b93b
commit
b392023686
@ -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,
|
||||
);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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::*;
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user