Fix `<Suspense/>` SSR

This commit is contained in:
Greg Johnston 2022-12-15 21:54:43 -05:00
parent c6d30a710a
commit 01252b841d
2 changed files with 71 additions and 59 deletions

View File

@ -1,3 +1,5 @@
use std::rc::Rc;
use leptos_dom::HydrationCtx;
use leptos_dom::{Fragment, IntoView};
use leptos_reactive::{provide_context, Scope, SuspenseContext};
@ -75,7 +77,7 @@ where
// provide this SuspenseContext to any resources below it
provide_context(cx, context);
render_suspense(cx, context, props.fallback, props.children)
render_suspense(cx, context, props.fallback, Rc::new(move |cx| (props.children)(cx)))
}
#[cfg(any(feature = "csr", feature = "hydrate"))]
@ -83,35 +85,20 @@ fn render_suspense<'a, F, E>(
cx: Scope,
context: SuspenseContext,
fallback: F,
child: Box<dyn Fn(Scope) -> Fragment>,
child: Rc<dyn Fn(Scope) -> Fragment>,
) -> impl IntoView
where
F: Fn() -> E + 'static,
E: IntoView,
{
use std::cell::RefCell;
use leptos_dom::DynChild;
let cached_id = RefCell::new(None);
let cached_id = HydrationCtx::peak();
DynChild::new(move || {
let mut cached_id_borrow = cached_id.borrow_mut();
let first_run = if cached_id_borrow.is_none() {
*cached_id_borrow = Some(HydrationCtx::peak());
true
} else {
false
};
if context.ready() {
leptos_dom::warn!("<Suspense/> ready");
if let Some(id) = *cached_id_borrow {
leptos_dom::warn!(" <Suspense/> continuing from {}", id);
HydrationCtx::continue_from(id);
}
leptos_dom::warn!("<Suspense/> ready and continuing from {}", cached_id);
HydrationCtx::continue_from(cached_id);
child(cx).into_view(cx)
} else {
@ -126,35 +113,47 @@ fn render_suspense<'a, F, E>(
cx: Scope,
context: SuspenseContext,
fallback: F,
orig_child: Box<dyn Fn(Scope) -> Fragment>,
orig_child: Rc<dyn Fn(Scope) -> Fragment>,
) -> impl IntoView
where
F: Fn() -> E + 'static,
E: IntoView,
{
use leptos_dom::*;
use std::cell::RefCell;
use leptos_dom::DynChild;
let orig_child = Rc::clone(&orig_child);
let current_id = HydrationCtx::peak();
DynChild::new(move || {
let initial = {
// run the child; we'll probably throw this away, but it will register resource reads
let child = orig_child(cx).into_view(cx);
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
child
}
// show the fallback, but also prepare to stream HTML
else {
let key = cx.current_fragment_key();
cx.register_suspense(context, &key, move || {
orig_child(cx)
.into_view(cx)
.render_to_string(cx)
.to_string()
});
// return the fallback for now, wrapped in fragment identifer
div(cx).id(key.to_string()).child(fallback).into_view(cx)
}
};
initial
let initial = {
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
child.clone()
}
// show the fallback, but also prepare to stream HTML
else {
let orig_child = Rc::clone(&orig_child);
cx.register_suspense(context, &current_id.to_string(), move || {
orig_child(cx)
.into_view(cx)
.render_to_string(cx)
.to_string()
});
// return the fallback for now, wrapped in fragment identifer
println!("continuing from...");
HydrationCtx::continue_from(current_id);
fallback().into_view(cx)
//Fragment::new(vec![fallback().into_view(cx)]).into_view(cx)
//div(cx).attr("data-l-fragment", key.to_string()).child(fallback).into_view(cx)
}
};
initial
}).into_view(cx)
}

View File

@ -112,9 +112,14 @@ pub fn render_to_stream_with_prefix(
r#"
<template id="{fragment_id}f">{html}</template>
<script>
var frag = document.getElementById("{fragment_id}");
var start = document.getElementById("_{fragment_id}o");
var end = document.getElementById("_{fragment_id}c");
var range = new Range();
range.setStartBefore(start.nextSibling);
range.setEndAfter(end.previousSibling);
range.deleteContents();
var tpl = document.getElementById("{fragment_id}f");
if(frag) frag.replaceWith(tpl.content.cloneNode(true));
end.parentNode.insertBefore(tpl.content.cloneNode(true), end);
</script>
"#
)
@ -169,20 +174,23 @@ impl View {
match self {
View::Text(node) => node.content,
View::Component(node) => {
let content = node
println!("rendering <{}/> with id {}", node.name, node.id);
let content = || node
.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("");
cfg_if! {
if #[cfg(debug_assertions)] {
format!(r#"<template id="{}"></template>{content}<template id="{}"></template>"#,
format!(r#"<template id="{}"></template>{}<template id="{}"></template>"#,
HydrationCtx::to_string(node.id, false),
content(),
HydrationCtx::to_string(node.id, true)
).into()
} else {
format!(
r#"{content}<template id="{}"></template>"#,
r#"{}<template id="{}"></template>"#,
content(),
HydrationCtx::to_string(node.id, true)
).into()
}
@ -193,18 +201,18 @@ impl View {
CoreComponent::Unit(u) => (
u.id,
false,
format!(
Box::new(move || format!(
"<template id={}></template>",
HydrationCtx::to_string(u.id, true)
)
.into(),
.into()) as Box<dyn FnOnce() -> Cow<'static, str>>,
),
CoreComponent::DynChild(node) => {
let child = node.child.take();
(
node.id,
true,
if let Some(child) = *child {
Box::new(move || if let Some(child) = *child {
// On debug builds, `DynChild` has two marker nodes,
// so there is no way for the text to be merged with
// surrounding text when the browser parses the HTML,
@ -223,7 +231,7 @@ impl View {
}
} else {
"".into()
},
}) as Box<dyn FnOnce() -> Cow<'static, str>>
)
}
CoreComponent::Each(node) => {
@ -232,30 +240,33 @@ impl View {
(
node.id,
true,
children
Box::new(move || children
.into_iter()
.flatten()
.map(|node| {
let id = node.id;
let content = node.child.render_to_string_helper();
let content = || node.child.render_to_string_helper();
#[cfg(debug_assertions)]
return format!(
"<template id=\"{}\"></template>{content}<template \
"<template id=\"{}\"></template>{}<template \
id=\"{}\"></template>",
HydrationCtx::to_string(id, false),
content(),
HydrationCtx::to_string(id, true),
);
#[cfg(not(debug_assertions))]
return format!(
"{content}<template id=\"{}c\"></template>",
"{}<template id=\"{}c\"></template>",
content(),
HydrationCtx::to_string(id, true)
);
})
.join("")
.into(),
.into()
) as Box<dyn FnOnce() -> Cow<'static, str>>,
)
}
};
@ -264,19 +275,21 @@ impl View {
cfg_if! {
if #[cfg(debug_assertions)] {
format!(
r#"<template id="{}"></template>{content}<template id="{}"></template>"#,
r#"<template id="{}"></template>{}<template id="{}"></template>"#,
HydrationCtx::to_string(id, false),
content(),
HydrationCtx::to_string(id, true),
).into()
} else {
format!(
r#"{content}<template id="{}"></template>"#,
r#"{}<template id="{}"></template>"#,
content(),
HydrationCtx::to_string(id, true)
).into()
}
}
} else {
content
content()
}
}
View::Element(el) => {