Get Suspense/Transition hydration working

This commit is contained in:
Greg Johnston 2022-12-18 07:38:51 -05:00
parent d071a7c1e0
commit 3195ab4ffc
13 changed files with 144 additions and 114 deletions

View file

@ -93,13 +93,14 @@ where
{
use leptos_dom::DynChild;
let cached_id = HydrationCtx::peak();
let _space_for_inner = HydrationCtx::id();
let cached_id = HydrationCtx::peek();
DynChild::new(move || {
if context.ready() {
leptos_dom::warn!("<Suspense/> ready and continuing from {}", cached_id);
HydrationCtx::continue_from(cached_id);
let mut id_to_replace = cached_id.clone();
id_to_replace.offset -= 1;
HydrationCtx::continue_from(id_to_replace);
child(cx).into_view(cx)
} else {
@ -123,7 +124,7 @@ where
use leptos_dom::DynChild;
let orig_child = Rc::clone(&orig_child);
let current_id = HydrationCtx::peak();
let current_id = HydrationCtx::peek();
DynChild::new(move || {
@ -138,7 +139,9 @@ where
// 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 || {
let mut id_to_replace = current_id.clone();
id_to_replace.offset += 1;
cx.register_suspense(context, &id_to_replace.to_string(), move || {
orig_child(cx)
.into_view(cx)
.render_to_string(cx)
@ -146,7 +149,7 @@ where
});
// return the fallback for now, wrapped in fragment identifer
HydrationCtx::continue_from(current_id);
HydrationCtx::continue_from(current_id.clone());
fallback().into_view(cx)
}
};

View file

@ -109,13 +109,14 @@ where
use std::cell::RefCell;
let prev_child = RefCell::new(None);
let cached_id = HydrationCtx::peak();
let _space_for_inner = HydrationCtx::id();
let cached_id = HydrationCtx::peek();
DynChild::new(move || {
if context.ready() {
leptos_dom::warn!("<Transition/> ready and continuing from {}", cached_id);
HydrationCtx::continue_from(cached_id);
let mut id_to_replace = cached_id.clone();
id_to_replace.offset -= 1;
HydrationCtx::continue_from(id_to_replace.clone());
let current_child = child(cx).into_view(cx);
*prev_child.borrow_mut() = Some(current_child.clone());
@ -152,7 +153,7 @@ where
E: IntoView,
{
let orig_child = Rc::clone(&orig_child);
let current_id = HydrationCtx::peak();
let current_id = HydrationCtx::peek();
DynChild::new(move || {
@ -167,7 +168,9 @@ where
// 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 || {
let mut id_to_replace = current_id.clone();
id_to_replace.offset += 1;
cx.register_suspense(context, &id_to_replace.to_string(), move || {
orig_child(cx)
.into_view(cx)
.render_to_string(cx)
@ -175,7 +178,7 @@ where
});
// return the fallback for now, wrapped in fragment identifer
HydrationCtx::continue_from(current_id);
HydrationCtx::continue_from(current_id.clone());
fallback().into_view(cx)
}
};

View file

@ -3,7 +3,7 @@ mod each;
mod fragment;
mod unit;
use crate::{hydration::HydrationCtx, Comment, IntoView, View};
use crate::{hydration::{HydrationCtx, HydrationKey}, Comment, IntoView, View};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use crate::{mount_child, MountKind, Mountable};
pub use dyn_child::*;
@ -51,7 +51,7 @@ pub struct ComponentRepr {
pub children: Vec<View>,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: usize,
pub(crate) id: HydrationKey,
}
impl fmt::Debug for ComponentRepr {
@ -125,17 +125,17 @@ impl IntoView for ComponentRepr {
impl ComponentRepr {
/// Creates a new [`Component`].
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self::new_with_id(name, HydrationCtx::id())
Self::new_with_id(name, HydrationCtx::next_component())
}
/// Creates a new [`Component`] with the given hydration ID.
pub fn new_with_id(name: impl Into<Cow<'static, str>>, id: usize) -> Self {
pub fn new_with_id(name: impl Into<Cow<'static, str>>, id: HydrationKey) -> Self {
let name = name.into();
let markers = (
Comment::new(Cow::Owned(format!("</{name}>")), id, true),
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
#[cfg(debug_assertions)]
Comment::new(Cow::Owned(format!("<{name}>")), id, false),
Comment::new(Cow::Owned(format!("<{name}>")), &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]

View file

@ -1,4 +1,4 @@
use crate::{hydration::HydrationCtx, Comment, IntoView, View};
use crate::{hydration::{HydrationCtx, HydrationKey}, Comment, IntoView, View};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use crate::{mount_child, unmount_child, MountKind, Mountable};
use leptos_reactive::Scope;
@ -18,7 +18,7 @@ pub struct DynChildRepr {
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: usize,
pub(crate) id: HydrationKey,
}
impl fmt::Debug for DynChildRepr {
@ -65,9 +65,9 @@ impl DynChildRepr {
let id = HydrationCtx::id();
let markers = (
Comment::new(Cow::Borrowed("</DynChild>"), id, true),
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
#[cfg(debug_assertions)]
Comment::new(Cow::Borrowed("<DynChild>"), id, false),
Comment::new(Cow::Borrowed("<DynChild>"), &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]

View file

@ -1,4 +1,4 @@
use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View};
use crate::{hydration::{HydrationCtx, HydrationKey}, Comment, CoreComponent, IntoView, View};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use crate::{mount_child, MountKind, Mountable, RANGE};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@ -50,7 +50,7 @@ pub struct EachRepr {
pub(crate) children: Rc<RefCell<Vec<Option<EachItem>>>>,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: usize,
pub(crate) id: HydrationKey,
}
impl fmt::Debug for EachRepr {
@ -74,9 +74,9 @@ impl Default for EachRepr {
let id = HydrationCtx::id();
let markers = (
Comment::new(Cow::Borrowed("</Each>"), id, true),
Comment::new(Cow::Borrowed("</Each>"), &id, true),
#[cfg(debug_assertions)]
Comment::new(Cow::Borrowed("<Each>"), id, false),
Comment::new(Cow::Borrowed("<Each>"), &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@ -147,7 +147,7 @@ pub(crate) struct EachItem {
pub(crate) child: View,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: usize,
pub(crate) id: HydrationKey,
}
impl fmt::Debug for EachItem {
@ -169,9 +169,9 @@ impl EachItem {
let id = HydrationCtx::id();
let markers = (
Comment::new(Cow::Borrowed("</EachItem>"), id, true),
Comment::new(Cow::Borrowed("</EachItem>"), &id, true),
#[cfg(debug_assertions)]
Comment::new(Cow::Borrowed("<EachItem>"), id, false),
Comment::new(Cow::Borrowed("<EachItem>"), &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@ -691,7 +691,7 @@ where
///
/// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// struct Counter {
/// id: usize,
/// id: HydrationKey,
/// count: RwSignal<i32>
/// }
///

View file

@ -1,6 +1,6 @@
use leptos_reactive::Scope;
use crate::{ComponentRepr, IntoView, View, HydrationCtx};
use crate::{ComponentRepr, IntoView, View, HydrationCtx, hydration::HydrationKey};
/// Trait for converting any iterable into a [`Fragment`].
pub trait IntoFragment {
@ -21,7 +21,7 @@ where
/// Represents a group of [`views`](View).
#[derive(Debug, Clone)]
pub struct Fragment {
id: usize,
id: HydrationKey,
nodes: Vec<View>
}
@ -43,8 +43,13 @@ impl Fragment {
Self::new_with_id(HydrationCtx::id(), nodes)
}
/// Creates a new [`Fragment`] from a function that returns [`Vec<Node>`].
pub fn lazy(nodes: impl Fn() -> Vec<View>) -> Self {
Self::new_with_id(HydrationCtx::id(), nodes())
}
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
pub fn new_with_id(id: usize, nodes: Vec<View>) -> Self {
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
Self {
id,
nodes
@ -57,15 +62,15 @@ impl Fragment {
}
/// Returns the fragment's hydration ID.
pub fn id(&self) -> usize {
self.id
pub fn id(&self) -> &HydrationKey {
&self.id
}
}
impl IntoView for Fragment {
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "</>", skip_all, fields(children = self.nodes.len())))]
fn into_view(self, cx: leptos_reactive::Scope) -> View {
let mut frag = ComponentRepr::new_with_id("", self.id);
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
frag.children = self.nodes;

View file

@ -2,7 +2,7 @@ use std::fmt;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use crate::Mountable;
use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View};
use crate::{hydration::{HydrationCtx, HydrationKey}, Comment, CoreComponent, IntoView, View};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::JsCast;
@ -11,7 +11,7 @@ use wasm_bindgen::JsCast;
pub struct UnitRepr {
comment: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: usize,
pub(crate) id: HydrationKey,
}
impl fmt::Debug for UnitRepr {
@ -25,7 +25,7 @@ impl Default for UnitRepr {
let id = HydrationCtx::id();
Self {
comment: Comment::new("<() />", id, true),
comment: Comment::new("<() />", &id, true),
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
}

View file

@ -1,3 +1,4 @@
use crate::hydration::HydrationKey;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use crate::events::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@ -70,7 +71,7 @@ pub trait IntoElement: IntoElementBounds {
/// A unique `id` that should be generated for each new instance of
/// this element, and be consitant for both SSR and CSR.
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
fn hydration_id(&self) -> usize;
fn hydration_id(&self) -> &HydrationKey;
}
/// Trait for converting any type which impl [`AsRef<web_sys::Element>`]
@ -115,7 +116,7 @@ pub struct AnyElement {
pub(crate) element: web_sys::HtmlElement,
pub(crate) is_void: bool,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: usize,
pub(crate) id: HydrationKey,
}
impl std::ops::Deref for AnyElement {
@ -145,8 +146,8 @@ impl IntoElement for AnyElement {
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
fn hydration_id(&self) -> usize {
self.id
fn hydration_id(&self) -> &HydrationKey {
&self.id
}
}
@ -157,7 +158,7 @@ pub struct Custom {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
element: web_sys::HtmlElement,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id: usize,
id: HydrationKey,
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@ -180,8 +181,8 @@ impl IntoElement for Custom {
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
fn hydration_id(&self) -> usize {
self.id
fn hydration_id(&self) -> &HydrationKey {
&self.id
}
}
@ -293,7 +294,7 @@ impl<El: IntoElement> HtmlElement<El> {
element: AnyElement {
name: element.name(),
is_void: element.is_void(),
id: element.hydration_id(),
id: element.hydration_id().clone(),
},
}
}
@ -571,7 +572,7 @@ impl<El: IntoElement> IntoView for HtmlElement<El> {
..
} = self;
let id = element.hydration_id();
let id = element.hydration_id().clone();
let mut element = Element::new(element);
let children = children;
@ -611,7 +612,7 @@ pub fn custom<El: IntoElement>(cx: Scope, el: El) -> HtmlElement<Custom> {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
element: el.get_element().clone(),
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id: el.hydration_id(),
id: el.hydration_id().clone(),
},
)
}
@ -644,7 +645,7 @@ macro_rules! generate_html_tags {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
element: web_sys::HtmlElement,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id: usize,
id: HydrationKey,
}
impl Default for [<$tag:camel $($trailing_)?>] {
@ -732,8 +733,8 @@ macro_rules! generate_html_tags {
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
fn hydration_id(&self) -> usize {
self.id
fn hydration_id(&self) -> &HydrationKey {
&self.id
}
generate_html_tags! { @void $($void)? }

View file

@ -1,4 +1,6 @@
use leptos_reactive::Scope;
use std::fmt::Display;
use std::cell::RefCell;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::cell::LazyCell;
@ -10,53 +12,72 @@ use std::cell::LazyCell;
#[thread_local]
static mut IS_HYDRATING: LazyCell<bool> = LazyCell::new(|| {
#[cfg(debug_assertions)]
return crate::document().get_element_by_id("_0").is_some()
|| crate::document().get_element_by_id("_0o").is_some();
return crate::document().get_element_by_id("_0-0-0").is_some()
|| crate::document().get_element_by_id("_0-0-0o").is_some();
#[cfg(not(debug_assertions))]
return crate::document().get_element_by_id("_0").is_some();
return crate::document().get_element_by_id("_0-0-0").is_some();
});
#[thread_local]
static mut ID: usize = 0;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HydrationKey {
pub previous: String,
pub offset: usize
}
#[thread_local]
static mut FORCE_HK: bool = false;
impl Display for HydrationKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.previous, self.offset)
}
}
impl Default for HydrationKey {
fn default() -> Self {
Self { previous: "0-".to_string(), offset: 0 }
}
}
thread_local!(static ID: RefCell<HydrationKey> = Default::default());
/// Control and utility methods for hydration.
pub struct HydrationCtx;
impl HydrationCtx {
/// Get the next `id` without incrementing it.
pub fn peak() -> usize {
unsafe { ID }
pub fn peek() -> HydrationKey {
ID.with(|id| id.borrow().clone())
}
/// Increments the current hydration `id` and returns it
pub fn id() -> usize {
unsafe {
let id = ID;
// Prevent panics on long-running debug builds
ID = ID.wrapping_add(1);
id
}
pub fn id() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
id.offset = id.offset.wrapping_add(1);
id.clone()
})
}
pub(crate) fn current_id() -> usize {
unsafe { ID }
/// Resets the hydration `id` for the next component, and returns it
pub fn next_component() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
let offset = id.offset;
id.previous.push_str(&offset.to_string());
id.previous.push('-');
id.offset = 0;
id.clone()
})
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) fn reset_id() {
unsafe { ID = 0 };
ID.with(|id| *id.borrow_mut() = Default::default());
}
/// Resums hydration from the provided `id`. Usefull for
/// `Suspense` and other fancy things.
pub fn continue_from(id: usize) {
unsafe { ID = id }
pub fn continue_from(id: HydrationKey) {
ID.with(|i| *i.borrow_mut() = id);
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@ -71,7 +92,7 @@ impl HydrationCtx {
unsafe { *IS_HYDRATING }
}
pub(crate) fn to_string(id: usize, closing: bool) -> String {
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
#[cfg(debug_assertions)]
return format!("_{id}{}", if closing { 'c' } else { 'o' });
@ -82,18 +103,4 @@ impl HydrationCtx {
format!("_{id}")
}
}
/// All components and elements created after this method is
/// called with use `leptos-hk` for their hydration `id`,
/// instead of `id`.
pub fn start_force_hk() {
unsafe { FORCE_HK = true }
}
/// All components and elements created after this method is
/// called with use `id` by default for their hydration `id`,
/// instead of `leptos-hk`.
pub fn stop_force_hk() {
unsafe { FORCE_HK = false }
}
}

View file

@ -25,6 +25,7 @@ pub use events::typed as ev;
pub use helpers::*;
pub use html::*;
pub use hydration::HydrationCtx;
use hydration::HydrationKey;
pub use js_sys;
use leptos_reactive::Scope;
pub use logging::*;
@ -150,7 +151,7 @@ cfg_if! {
attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
children: Vec<View>,
prerendered: Option<Cow<'static, str>>,
id: usize,
id: HydrationKey,
}
impl fmt::Debug for Element {
@ -245,7 +246,7 @@ impl Element {
is_void: el.is_void(),
attrs: Default::default(),
children: Default::default(),
id: el.hydration_id(),
id: el.hydration_id().clone(),
prerendered: None
}
}
@ -263,7 +264,7 @@ impl Element {
is_void: el.is_void(),
attrs: Default::default(),
children: Default::default(),
id: el.hydration_id(),
id: el.hydration_id().clone(),
prerendered: Some(html.into()),
}
}
@ -279,7 +280,7 @@ struct Comment {
impl Comment {
fn new(
content: impl Into<Cow<'static, str>>,
id: usize,
id: &HydrationKey,
closing: bool,
) -> Self {
let content = content.into();

View file

@ -182,9 +182,9 @@ impl View {
cfg_if! {
if #[cfg(debug_assertions)] {
format!(r#"<template id="{}"></template>{}<template id="{}"></template>"#,
HydrationCtx::to_string(node.id, false),
HydrationCtx::to_string(&node.id, false),
content(),
HydrationCtx::to_string(node.id, true)
HydrationCtx::to_string(&node.id, true)
).into()
} else {
format!(
@ -198,11 +198,11 @@ impl View {
View::CoreComponent(node) => {
let (id, wrap, content) = match node {
CoreComponent::Unit(u) => (
u.id,
u.id.clone(),
false,
Box::new(move || format!(
"<template id={}></template>",
HydrationCtx::to_string(u.id, true)
HydrationCtx::to_string(&u.id, true)
)
.into()) as Box<dyn FnOnce() -> Cow<'static, str>>,
),
@ -251,9 +251,9 @@ impl View {
return format!(
"<template id=\"{}\"></template>{}<template \
id=\"{}\"></template>",
HydrationCtx::to_string(id, false),
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(id, true),
HydrationCtx::to_string(&id, true),
);
#[cfg(not(debug_assertions))]
@ -275,15 +275,15 @@ impl View {
if #[cfg(debug_assertions)] {
format!(
r#"<template id="{}"></template>{}<template id="{}"></template>"#,
HydrationCtx::to_string(id, false),
HydrationCtx::to_string(&id, false),
content(),
HydrationCtx::to_string(id, true),
HydrationCtx::to_string(&id, true),
).into()
} else {
format!(
r#"{}<template id="{}"></template>"#,
content(),
HydrationCtx::to_string(id, true)
HydrationCtx::to_string(&id, true)
).into()
}
}

View file

@ -155,7 +155,7 @@ pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream
} else if nodes.len() == 1 {
node_to_tokens(cx, &nodes[0])
} else {
fragment_to_tokens(cx, Span::call_site(), nodes)
fragment_to_tokens(cx, Span::call_site(), nodes, false)
}
}
}
@ -475,7 +475,7 @@ fn set_class_attribute_ssr(
}
}
fn fragment_to_tokens(cx: &Ident, span: Span, nodes: &[Node]) -> TokenStream {
fn fragment_to_tokens(cx: &Ident, span: Span, nodes: &[Node], lazy: bool) -> TokenStream {
let nodes = nodes.iter().map(|node| {
let node = node_to_tokens(cx, node);
@ -483,18 +483,28 @@ fn fragment_to_tokens(cx: &Ident, span: Span, nodes: &[Node]) -> TokenStream {
#node.into_view(#cx)
}
});
quote! {
{
leptos::Fragment::new(vec![
#(#nodes),*
])
if lazy {
quote! {
{
leptos::Fragment::lazy(|| vec![
#(#nodes),*
])
}
}
} else {
quote! {
{
leptos::Fragment::new(vec![
#(#nodes),*
])
}
}
}
}
fn node_to_tokens(cx: &Ident, node: &Node) -> TokenStream {
match node {
Node::Fragment(fragment) => fragment_to_tokens(cx, Span::call_site(), &fragment.children),
Node::Fragment(fragment) => fragment_to_tokens(cx, Span::call_site(), &fragment.children, false),
Node::Comment(_) | Node::Doctype(_) => quote! {},
Node::Text(node) => {
let value = node.value.as_ref();
@ -533,7 +543,7 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement) -> TokenStream {
let children = node.children.iter().map(|node| {
let child = match node {
Node::Fragment(fragment) => {
fragment_to_tokens(cx, Span::call_site(), &fragment.children)
fragment_to_tokens(cx, Span::call_site(), &fragment.children, false)
}
Node::Text(node) => {
let span = node.value.span();
@ -644,7 +654,7 @@ fn component_to_tokens(cx: &Ident, node: &NodeElement) -> TokenStream {
let children = if node.children.is_empty() {
quote! {}
} else {
let children = fragment_to_tokens(cx, span, &node.children);
let children = fragment_to_tokens(cx, span, &node.children, true);
quote! { .children(Box::new(move |#cx| #children)) }
};

View file

@ -200,7 +200,7 @@ pub fn Routes(
});
Fragment::new_with_id(
frag.id(),
frag.id().clone(),
vec![(move || root.get()).into_view(cx)]
)
}