Fix hydration of suspense fragments that contain text nodes (#3287)
* Fix hydration of suspense fragments that contain text nodes * fix release builds * disable link deduplication * restore meta tag deduplication * Add a test for the failed hydration case * make changes to the document trait non-breaking * remove info log from server future hook * restore default implementation - this was also a breaking change * fix semver breakage * clean up server_future * Cleanup unused code * fix clippy --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
57d637184f
commit
d41ab3a756
33 files changed
+644
-222
No files matched your search
@@ -59,12 +59,13 @@ impl ServerDocument {
|
||||
|
||||
/// Write the head element into the serialized context for hydration
|
||||
/// We write true if the head element was written to the DOM during server side rendering
|
||||
#[track_caller]
|
||||
pub(crate) fn serialize_for_hydration(&self) {
|
||||
// We only serialize the head elements if the web document feature is enabled
|
||||
#[cfg(feature = "document")]
|
||||
{
|
||||
let serialize = crate::html_storage::serialize_context();
|
||||
serialize.push(&!self.0.borrow().streaming);
|
||||
serialize.push(&!self.0.borrow().streaming, std::panic::Location::caller());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,13 +77,10 @@ impl Document for ServerDocument {
|
||||
|
||||
fn set_title(&self, title: String) {
|
||||
self.warn_if_streaming();
|
||||
self.serialize_for_hydration();
|
||||
self.0.borrow_mut().title = Some(title);
|
||||
}
|
||||
|
||||
fn create_meta(&self, props: MetaProps) {
|
||||
self.warn_if_streaming();
|
||||
self.serialize_for_hydration();
|
||||
self.0.borrow_mut().meta.push(rsx! {
|
||||
meta {
|
||||
name: props.name,
|
||||
@@ -96,8 +94,6 @@ impl Document for ServerDocument {
|
||||
}
|
||||
|
||||
fn create_script(&self, props: ScriptProps) {
|
||||
self.warn_if_streaming();
|
||||
self.serialize_for_hydration();
|
||||
let children = props.script_contents().ok();
|
||||
self.0.borrow_mut().script.push(rsx! {
|
||||
script {
|
||||
@@ -117,42 +113,19 @@ impl Document for ServerDocument {
|
||||
}
|
||||
|
||||
fn create_style(&self, props: StyleProps) {
|
||||
self.warn_if_streaming();
|
||||
self.serialize_for_hydration();
|
||||
match (&props.href, props.style_contents()) {
|
||||
// The style has inline contents, render it as a style tag
|
||||
(_, Ok(contents)) => self.0.borrow_mut().script.push(rsx! {
|
||||
style {
|
||||
media: props.media,
|
||||
nonce: props.nonce,
|
||||
title: props.title,
|
||||
..props.additional_attributes,
|
||||
{contents}
|
||||
}
|
||||
}),
|
||||
// The style has a href, render it as a link tag
|
||||
(Some(_), _) => {
|
||||
self.0.borrow_mut().script.push(rsx! {
|
||||
link {
|
||||
rel: "stylesheet",
|
||||
href: props.href,
|
||||
media: props.media,
|
||||
nonce: props.nonce,
|
||||
title: props.title,
|
||||
..props.additional_attributes,
|
||||
}
|
||||
});
|
||||
let contents = props.style_contents().ok();
|
||||
self.0.borrow_mut().script.push(rsx! {
|
||||
style {
|
||||
media: props.media,
|
||||
nonce: props.nonce,
|
||||
title: props.title,
|
||||
..props.additional_attributes,
|
||||
{contents}
|
||||
}
|
||||
// The style has neither contents nor src, log an error
|
||||
(None, Err(err)) => {
|
||||
err.log("Style");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn create_link(&self, props: LinkProps) {
|
||||
self.warn_if_streaming();
|
||||
self.serialize_for_hydration();
|
||||
self.0.borrow_mut().link.push(rsx! {
|
||||
link {
|
||||
rel: props.rel,
|
||||
@@ -172,4 +145,10 @@ impl Document for ServerDocument {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn create_head_component(&self) -> bool {
|
||||
self.warn_if_streaming();
|
||||
self.serialize_for_hydration();
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#![allow(unused)]
|
||||
//! On the client, we use the [`WebDocument`] implementation to render the head for any elements that were not rendered on the server.
|
||||
|
||||
use dioxus_lib::document::*;
|
||||
use dioxus_lib::{document::*, prelude::queue_effect};
|
||||
use dioxus_web::WebDocument;
|
||||
|
||||
fn head_element_written_on_server() -> bool {
|
||||
@@ -12,6 +12,7 @@ fn head_element_written_on_server() -> bool {
|
||||
}
|
||||
|
||||
/// A document provider for fullstack web clients
|
||||
#[derive(Clone)]
|
||||
pub struct FullstackWebDocument;
|
||||
|
||||
impl Document for FullstackWebDocument {
|
||||
@@ -19,38 +20,32 @@ impl Document for FullstackWebDocument {
|
||||
WebDocument.eval(js)
|
||||
}
|
||||
|
||||
/// Set the title of the document
|
||||
fn set_title(&self, title: String) {
|
||||
if head_element_written_on_server() {
|
||||
return;
|
||||
}
|
||||
WebDocument.set_title(title);
|
||||
}
|
||||
|
||||
/// Create a new meta tag in the head
|
||||
fn create_meta(&self, props: MetaProps) {
|
||||
if head_element_written_on_server() {
|
||||
return;
|
||||
}
|
||||
WebDocument.create_meta(props);
|
||||
}
|
||||
|
||||
/// Create a new script tag in the head
|
||||
fn create_script(&self, props: ScriptProps) {
|
||||
if head_element_written_on_server() {
|
||||
return;
|
||||
}
|
||||
WebDocument.create_script(props);
|
||||
}
|
||||
|
||||
/// Create a new style tag in the head
|
||||
fn create_style(&self, props: StyleProps) {
|
||||
if head_element_written_on_server() {
|
||||
return;
|
||||
}
|
||||
WebDocument.create_style(props);
|
||||
}
|
||||
|
||||
/// Create a new link tag in the head
|
||||
fn create_link(&self, props: LinkProps) {
|
||||
if head_element_written_on_server() {
|
||||
return;
|
||||
}
|
||||
WebDocument.create_link(props);
|
||||
}
|
||||
|
||||
fn create_head_component(&self) -> bool {
|
||||
!head_element_written_on_server()
|
||||
}
|
||||
}
|
||||
@@ -20,20 +20,23 @@ use serde::{de::DeserializeOwned, Serialize};
|
||||
/// unimplemented!()
|
||||
/// }
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn use_server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
||||
server_fn: impl Fn() -> O,
|
||||
) -> O {
|
||||
use_hook(|| server_cached(server_fn))
|
||||
let location = std::panic::Location::caller();
|
||||
use_hook(|| server_cached(server_fn, location))
|
||||
}
|
||||
|
||||
pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
||||
value: impl FnOnce() -> O,
|
||||
#[allow(unused)] location: &'static std::panic::Location<'static>,
|
||||
) -> O {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
let serialize = crate::html_storage::serialize_context();
|
||||
let data = value();
|
||||
serialize.push(&data);
|
||||
serialize.push(&data, location);
|
||||
data
|
||||
}
|
||||
#[cfg(all(not(feature = "server"), feature = "web"))]
|
||||
|
||||
@@ -56,6 +56,7 @@ use std::future::Future;
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
||||
#[track_caller]
|
||||
pub fn use_server_future<T, F>(
|
||||
mut future: impl FnMut() -> F + 'static,
|
||||
) -> Result<Resource<T>, RenderError>
|
||||
@@ -65,10 +66,14 @@ where
|
||||
{
|
||||
#[cfg(feature = "server")]
|
||||
let serialize_context = crate::html_storage::use_serialize_context();
|
||||
|
||||
// We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
||||
#[cfg(feature = "server")]
|
||||
let server_storage_entry = use_hook(|| serialize_context.create_entry());
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
let caller = std::panic::Location::caller();
|
||||
|
||||
// If this is the first run and we are on the web client, the data might be cached
|
||||
#[cfg(feature = "web")]
|
||||
let initial_web_result = use_hook(|| {
|
||||
@@ -80,33 +85,28 @@ where
|
||||
let resource = use_resource(move || {
|
||||
#[cfg(feature = "server")]
|
||||
let serialize_context = serialize_context.clone();
|
||||
|
||||
let user_fut = future();
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
let initial_web_result = initial_web_result.clone();
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
async move {
|
||||
// If this is the first run and we are on the web client, the data might be cached
|
||||
#[cfg(feature = "web")]
|
||||
{
|
||||
let initial = initial_web_result.borrow_mut().take();
|
||||
match initial {
|
||||
// This isn't the first run
|
||||
None => {}
|
||||
// This is the first run
|
||||
Some(first_run) => {
|
||||
match first_run {
|
||||
// THe data was deserialized successfully from the server
|
||||
Ok(Some(o)) => return o,
|
||||
// The data is still pending from the server. Don't try to resolve it on the client
|
||||
Ok(None) => {
|
||||
tracing::trace!("Waiting for server data");
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
// The data was not available on the server, rerun the future
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
match initial_web_result.take() {
|
||||
// The data was deserialized successfully from the server
|
||||
Some(Ok(Some(o))) => return o,
|
||||
|
||||
// The data is still pending from the server. Don't try to resolve it on the client
|
||||
Some(Ok(None)) => std::future::pending::<()>().await,
|
||||
|
||||
// The data was not available on the server, rerun the future
|
||||
Some(Err(_)) => {}
|
||||
|
||||
// This isn't the first run, so we don't need do anything
|
||||
None => {}
|
||||
}
|
||||
|
||||
// Otherwise just run the future itself
|
||||
@@ -114,9 +114,8 @@ where
|
||||
|
||||
// If this is the first run and we are on the server, cache the data in the slot we reserved for it
|
||||
#[cfg(feature = "server")]
|
||||
serialize_context.insert(server_storage_entry, &out);
|
||||
serialize_context.insert(server_storage_entry, &out, caller);
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
out
|
||||
}
|
||||
});
|
||||
@@ -127,14 +126,12 @@ where
|
||||
});
|
||||
|
||||
// Suspend if the value isn't ready
|
||||
match resource.state().cloned() {
|
||||
UseResourceState::Pending => {
|
||||
let task = resource.task();
|
||||
if !task.paused() {
|
||||
return Err(suspend(task).unwrap_err());
|
||||
}
|
||||
Ok(resource)
|
||||
if resource.state().cloned() == UseResourceState::Pending {
|
||||
let task = resource.task();
|
||||
if !task.paused() {
|
||||
return Err(suspend(task).unwrap_err());
|
||||
}
|
||||
_ => Ok(resource),
|
||||
}
|
||||
|
||||
Ok(resource)
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
#![allow(unused)]
|
||||
use base64::Engine;
|
||||
use dioxus_lib::prelude::{has_context, provide_context, use_hook};
|
||||
use serialize::serde_to_writable;
|
||||
use std::{cell::RefCell, io::Cursor, rc::Rc, sync::atomic::AtomicUsize};
|
||||
#![cfg(feature = "server")]
|
||||
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use dioxus_lib::prelude::{has_context, provide_context, use_hook};
|
||||
use serde::Serialize;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
pub(crate) mod serialize;
|
||||
|
||||
@@ -21,13 +18,22 @@ impl SerializeContext {
|
||||
}
|
||||
|
||||
/// Insert data into an entry that was created with [`Self::create_entry`]
|
||||
pub(crate) fn insert<T: Serialize>(&self, id: usize, value: &T) {
|
||||
self.data.borrow_mut().insert(id, value);
|
||||
pub(crate) fn insert<T: Serialize>(
|
||||
&self,
|
||||
id: usize,
|
||||
value: &T,
|
||||
location: &'static std::panic::Location<'static>,
|
||||
) {
|
||||
self.data.borrow_mut().insert(id, value, location);
|
||||
}
|
||||
|
||||
/// Push resolved data into the serialized server data
|
||||
pub(crate) fn push<T: Serialize>(&self, data: &T) {
|
||||
self.data.borrow_mut().push(data);
|
||||
pub(crate) fn push<T: Serialize>(
|
||||
&self,
|
||||
data: &T,
|
||||
location: &'static std::panic::Location<'static>,
|
||||
) {
|
||||
self.data.borrow_mut().push(data, location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,31 +45,73 @@ pub(crate) fn serialize_context() -> SerializeContext {
|
||||
has_context().unwrap_or_else(|| provide_context(SerializeContext::default()))
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default)]
|
||||
#[serde(transparent)]
|
||||
#[derive(Default)]
|
||||
pub(crate) struct HTMLData {
|
||||
/// The data required for hydration
|
||||
pub data: Vec<Option<Vec<u8>>>,
|
||||
/// The types of each serialized data
|
||||
///
|
||||
/// NOTE: we don't store this in the main data vec because we don't want to include it in
|
||||
/// release mode and we can't assume both the client and server are built with debug assertions
|
||||
/// matching
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_types: Vec<Option<String>>,
|
||||
/// The locations of each serialized data
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_locations: Vec<Option<String>>,
|
||||
}
|
||||
|
||||
impl HTMLData {
|
||||
/// Create a new entry in the data that will be sent to the client without inserting any data. Returns an id that can be used to insert data into the entry once it is ready.
|
||||
pub(crate) fn create_entry(&mut self) -> usize {
|
||||
fn create_entry(&mut self) -> usize {
|
||||
let id = self.data.len();
|
||||
self.data.push(None);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_types.push(None);
|
||||
self.debug_locations.push(None);
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
/// Insert data into an entry that was created with [`Self::create_entry`]
|
||||
pub(crate) fn insert<T: Serialize>(&mut self, id: usize, value: &T) {
|
||||
fn insert<T: Serialize>(
|
||||
&mut self,
|
||||
id: usize,
|
||||
value: &T,
|
||||
location: &'static std::panic::Location<'static>,
|
||||
) {
|
||||
let mut serialized = Vec::new();
|
||||
ciborium::into_writer(value, &mut serialized).unwrap();
|
||||
self.data[id] = Some(serialized);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_types[id] = Some(std::any::type_name::<T>().to_string());
|
||||
self.debug_locations[id] = Some(location.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Push resolved data into the serialized server data
|
||||
pub(crate) fn push<T: Serialize>(&mut self, data: &T) {
|
||||
fn push<T: Serialize>(&mut self, data: &T, location: &'static std::panic::Location<'static>) {
|
||||
let mut serialized = Vec::new();
|
||||
ciborium::into_writer(data, &mut serialized).unwrap();
|
||||
self.data.push(Some(serialized));
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_types
|
||||
.push(Some(std::any::type_name::<T>().to_string()));
|
||||
self.debug_locations.push(Some(location.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend this data with the data from another [`HTMLData`]
|
||||
pub(crate) fn extend(&mut self, other: &Self) {
|
||||
self.data.extend_from_slice(&other.data);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_types.extend_from_slice(&other.debug_types);
|
||||
self.debug_locations
|
||||
.extend_from_slice(&other.debug_locations);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,29 @@
|
||||
use dioxus_lib::prelude::dioxus_core::DynamicNode;
|
||||
use dioxus_lib::prelude::{
|
||||
has_context, try_consume_context, ErrorContext, ScopeId, SuspenseBoundaryProps,
|
||||
SuspenseContext, VNode, VirtualDom,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use dioxus_lib::prelude::dioxus_core::DynamicNode;
|
||||
use dioxus_lib::prelude::{has_context, ErrorContext, ScopeId, SuspenseContext, VNode, VirtualDom};
|
||||
|
||||
use super::SerializeContext;
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn serde_to_writable<T: Serialize>(
|
||||
value: &T,
|
||||
write_to: &mut impl std::fmt::Write,
|
||||
) -> Result<(), ciborium::ser::Error<std::fmt::Error>> {
|
||||
let mut serialized = Vec::new();
|
||||
ciborium::into_writer(value, &mut serialized).unwrap();
|
||||
write_to.write_str(STANDARD.encode(serialized).as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl super::HTMLData {
|
||||
/// Walks through the suspense boundary in a depth first order and extracts the data from the context API.
|
||||
/// We use depth first order instead of relying on the order the hooks are called in because during suspense on the server, the order that futures are run in may be non deterministic.
|
||||
pub(crate) fn extract_from_suspense_boundary(vdom: &VirtualDom, scope: ScopeId) -> Self {
|
||||
let mut data = Self::default();
|
||||
// If there is an error boundary on the suspense boundary, grab the error from the context API
|
||||
// and throw it on the client so that it bubbles up to the nearest error boundary
|
||||
let mut error = vdom.in_runtime(|| {
|
||||
scope
|
||||
.consume_context::<ErrorContext>()
|
||||
.and_then(|error_context| error_context.errors().first().cloned())
|
||||
});
|
||||
data.push(&error);
|
||||
data.serialize_errors(vdom, scope);
|
||||
data.take_from_scope(vdom, scope);
|
||||
data
|
||||
}
|
||||
|
||||
fn take_from_virtual_dom(&mut self, vdom: &VirtualDom) {
|
||||
self.take_from_scope(vdom, ScopeId::ROOT)
|
||||
/// Get the errors from the suspense boundary
|
||||
fn serialize_errors(&mut self, vdom: &VirtualDom, scope: ScopeId) {
|
||||
// If there is an error boundary on the suspense boundary, grab the error from the context API
|
||||
// and throw it on the client so that it bubbles up to the nearest error boundary
|
||||
let error = vdom.in_runtime(|| {
|
||||
scope
|
||||
.consume_context::<ErrorContext>()
|
||||
.and_then(|error_context| error_context.errors().first().cloned())
|
||||
});
|
||||
self.push(&error, std::panic::Location::caller());
|
||||
}
|
||||
|
||||
fn take_from_scope(&mut self, vdom: &VirtualDom, scope: ScopeId) {
|
||||
@@ -48,9 +32,7 @@ impl super::HTMLData {
|
||||
// Grab any serializable server context from this scope
|
||||
let context: Option<SerializeContext> = has_context();
|
||||
if let Some(context) = context {
|
||||
let borrow = context.data.borrow();
|
||||
let mut data = borrow.data.iter().cloned();
|
||||
self.data.extend(data)
|
||||
self.extend(&context.data.borrow());
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -91,9 +73,44 @@ impl super::HTMLData {
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
/// Encode data as base64. This is intended to be used in the server to send data to the client.
|
||||
pub(crate) fn serialized(&self) -> String {
|
||||
pub(crate) fn serialized(&self) -> SerializedHydrationData {
|
||||
let mut serialized = Vec::new();
|
||||
ciborium::into_writer(&self.data, &mut serialized).unwrap();
|
||||
base64::engine::general_purpose::STANDARD.encode(serialized)
|
||||
|
||||
let data = base64::engine::general_purpose::STANDARD.encode(serialized);
|
||||
|
||||
let format_js_list_of_strings = |list: &[Option<String>]| {
|
||||
let body = list
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
Some(s) => format!(r#""{s}""#),
|
||||
None => r#""unknown""#.to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
format!("[{}]", body)
|
||||
};
|
||||
|
||||
SerializedHydrationData {
|
||||
data,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_types: format_js_list_of_strings(&self.debug_types),
|
||||
#[cfg(debug_assertions)]
|
||||
debug_locations: format_js_list_of_strings(&self.debug_locations),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
/// Data that was serialized on the server for hydration on the client. This includes
|
||||
/// extra information about the types and sources of the serialized data in debug mode
|
||||
pub(crate) struct SerializedHydrationData {
|
||||
/// The base64 encoded serialized data
|
||||
pub data: String,
|
||||
/// A list of the types of each serialized data
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_types: String,
|
||||
/// A list of the locations of each serialized data
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_locations: String,
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! A shared pool of renderers for efficient server side rendering.
|
||||
use crate::document::ServerDocument;
|
||||
use crate::html_storage::serialize::SerializedHydrationData;
|
||||
use crate::streaming::{Mount, StreamingRenderer};
|
||||
use dioxus_cli_config::base_path;
|
||||
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
|
||||
@@ -408,7 +409,7 @@ fn start_capturing_errors(suspense_scope: ScopeId) {
|
||||
suspense_scope.in_runtime(provide_error_boundary);
|
||||
}
|
||||
|
||||
fn serialize_server_data(virtual_dom: &VirtualDom, scope: ScopeId) -> String {
|
||||
fn serialize_server_data(virtual_dom: &VirtualDom, scope: ScopeId) -> SerializedHydrationData {
|
||||
// After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node
|
||||
// Extract any data we serialized for hydration (from server futures)
|
||||
let html_data =
|
||||
@@ -530,10 +531,27 @@ impl FullstackHTMLTemplate {
|
||||
// Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s.
|
||||
// Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend
|
||||
let resolved_data = serialize_server_data(virtual_dom, ScopeId::ROOT);
|
||||
// We always send down the data required to hydrate components on the client
|
||||
let raw_data = resolved_data.data;
|
||||
write!(
|
||||
to,
|
||||
r#"<script>window.initial_dioxus_hydration_data="{resolved_data}";</script>"#,
|
||||
r#"<script>window.initial_dioxus_hydration_data="{raw_data}";"#,
|
||||
)?;
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// In debug mode, we also send down the type names and locations of the serialized data
|
||||
let debug_types = &resolved_data.debug_types;
|
||||
let debug_locations = &resolved_data.debug_locations;
|
||||
write!(
|
||||
to,
|
||||
r#"window.initial_dioxus_hydration_debug_types={debug_types};"#,
|
||||
)?;
|
||||
write!(
|
||||
to,
|
||||
r#"window.initial_dioxus_hydration_debug_locations={debug_locations};"#,
|
||||
)?;
|
||||
}
|
||||
write!(to, r#"</script>"#,)?;
|
||||
to.write_str(&index.post_main)?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -33,6 +33,8 @@ use std::{
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use crate::html_storage::serialize::SerializedHydrationData;
|
||||
|
||||
/// Sections are identified by a unique id based on the suspense path. We only track the path of suspense boundaries because the client may render different components than the server.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct MountPath {
|
||||
@@ -109,7 +111,7 @@ impl<E> StreamingRenderer<E> {
|
||||
&self,
|
||||
id: Mount,
|
||||
html: impl FnOnce(&mut W) -> std::fmt::Result,
|
||||
data: impl Display,
|
||||
resolved_data: SerializedHydrationData,
|
||||
into: &mut W,
|
||||
) -> std::fmt::Result {
|
||||
// Then replace the suspense placeholder with the new content
|
||||
@@ -119,10 +121,27 @@ impl<E> StreamingRenderer<E> {
|
||||
html(into)?;
|
||||
// Restore the old path
|
||||
*self.current_path.write().unwrap() = old_path;
|
||||
// dx_hydrate accepts 2-4 arguments. The first two are required, the rest are optional
|
||||
// The arguments are:
|
||||
// 1. The id of the nodes we are hydrating under
|
||||
// 2. The serialized data required to hydrate those components
|
||||
// 3. (in debug mode) The type names of the serialized data
|
||||
// 4. (in debug mode) The locations of the serialized data
|
||||
let raw_data = resolved_data.data;
|
||||
write!(
|
||||
into,
|
||||
r#"</div><script>window.dx_hydrate([{id}], "{data}")</script>"#
|
||||
)
|
||||
r#"</div><script>window.dx_hydrate([{id}], "{raw_data}""#
|
||||
)?;
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// In debug mode, we also send down the type names and locations of the serialized data
|
||||
let debug_types = &resolved_data.debug_types;
|
||||
let debug_locations = &resolved_data.debug_locations;
|
||||
write!(into, r#", {debug_types}, {debug_locations}"#,)?;
|
||||
}
|
||||
write!(into, r#")</script>"#)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close the stream with an error
|
||||
|
||||
Reference in new issue
Block a user