Skip to content

Commit

Permalink
docs: adds more helpful text for hydration errors (closes #3267) (#3275)
Browse files Browse the repository at this point in the history
  • Loading branch information
gbj authored Nov 22, 2024
1 parent 75cae91 commit 3de0414
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 35 deletions.
3 changes: 2 additions & 1 deletion tachys/src/html/element/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ where
E: AsRef<str>,
{
HtmlElement {
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
tag: Custom(tag),

attributes: (),
children: (),
}
Expand Down
30 changes: 22 additions & 8 deletions tachys/src/html/element/elements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ macro_rules! html_element_inner {

{
HtmlElement {
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
tag: $struct_name,
attributes: (),
children: (),

}
}

Expand All @@ -55,10 +56,17 @@ macro_rules! html_element_inner {
At: NextTuple,
<At as NextTuple>::Output<Attr<$crate::html::attribute::[<$attr:camel>], V>>: Attribute,
{
let HtmlElement { tag, children, attributes } = self;
let HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,
children,
attributes
} = self;
HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,

children,
attributes: attributes.next_tuple($crate::html::attribute::$attr(value)),
}
Expand Down Expand Up @@ -118,14 +126,16 @@ macro_rules! html_self_closing_elements {
paste::paste! {
$(
#[$meta]
#[track_caller]
pub fn $tag() -> HtmlElement<[<$tag:camel>], (), ()>
where

{
HtmlElement {
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
attributes: (),
children: (),

tag: [<$tag:camel>],
}
}
Expand All @@ -138,7 +148,6 @@ macro_rules! html_self_closing_elements {
impl<At> HtmlElement<[<$tag:camel>], At, ()>
where
At: Attribute,

{
$(
#[doc = concat!("The [`", stringify!($attr), "`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/", stringify!($tag), "#", stringify!($attr) ,") attribute on `<", stringify!($tag), ">`.")]
Expand All @@ -151,13 +160,18 @@ macro_rules! html_self_closing_elements {
V: AttributeValue,
At: NextTuple,
<At as NextTuple>::Output<Attr<$crate::html::attribute::[<$attr:camel>], V>>: Attribute,

{
let HtmlElement { tag, children, attributes,
let HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,
children,
attributes,
} = self;
HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,

children,
attributes: attributes.next_tuple($crate::html::attribute::$attr(value)),
}
Expand Down
33 changes: 27 additions & 6 deletions tachys/src/html/element/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#[cfg(debug_assertions)]
use crate::hydration::set_currently_hydrating;
use crate::{
html::attribute::Attribute,
hydration::Cursor,
hydration::{failed_to_cast_element, Cursor},
renderer::{CastFrom, Rndr},
ssr::StreamBuilder,
view::{
Expand All @@ -24,10 +26,14 @@ pub use custom::*;
pub use element_ext::*;
pub use elements::*;
pub use inner_html::*;
#[cfg(debug_assertions)]
use std::panic::Location;

/// The typed representation of an HTML element.
#[derive(Debug, PartialEq, Eq)]
pub struct HtmlElement<E, At, Ch> {
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static Location<'static>,
pub(crate) tag: E,
pub(crate) attributes: At,
pub(crate) children: Ch,
Expand All @@ -36,8 +42,9 @@ pub struct HtmlElement<E, At, Ch> {
impl<E: Clone, At: Clone, Ch: Clone> Clone for HtmlElement<E, At, Ch> {
fn clone(&self) -> Self {
HtmlElement {
#[cfg(debug_assertions)]
defined_at: self.defined_at,
tag: self.tag.clone(),

attributes: self.attributes.clone(),
children: self.children.clone(),
}
Expand Down Expand Up @@ -75,14 +82,16 @@ where

fn child(self, child: NewChild) -> Self::Output {
let HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,

attributes,
children,
} = self;
HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,

attributes,
children: children.next_tuple(child.into_render()),
}
Expand All @@ -103,11 +112,15 @@ where
attr: NewAttr,
) -> Self::Output<NewAttr> {
let HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,
attributes,
children,
} = self;
HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,
attributes: attributes.add_any_attr(attr),
children,
Expand Down Expand Up @@ -229,8 +242,9 @@ where
let (attributes, children) =
join(self.attributes.resolve(), self.children.resolve()).await;
HtmlElement {
#[cfg(debug_assertions)]
defined_at: self.defined_at,
tag: self.tag,

attributes,
children,
}
Expand Down Expand Up @@ -336,6 +350,11 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
#[cfg(debug_assertions)]
{
set_currently_hydrating(Some(self.defined_at));
}

// non-Static custom elements need special support in templates
// because they haven't been inserted type-wise
if E::TAG.is_empty() && !FROM_SERVER {
Expand All @@ -349,7 +368,9 @@ where
cursor.sibling();
}
let el = crate::renderer::types::Element::cast_from(cursor.current())
.unwrap();
.unwrap_or_else(|| {
failed_to_cast_element(E::TAG, cursor.current())
});

let attrs = self.attributes.hydrate::<FROM_SERVER>(&el);

Expand Down
129 changes: 120 additions & 9 deletions tachys/src/hydration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use crate::{
renderer::{CastFrom, Rndr},
view::{Position, PositionState},
};
use std::{cell::RefCell, rc::Rc};
#[cfg(debug_assertions)]
use std::cell::Cell;
use std::{cell::RefCell, panic::Location, rc::Rc};
use web_sys::{Comment, Element, Node, Text};

/// Hydration works by walking over the DOM, adding interactivity as needed.
///
Expand Down Expand Up @@ -95,13 +98,121 @@ where
}
let marker = self.current();
position.set(Position::NextChild);
crate::renderer::types::Placeholder::cast_from(marker)
.expect("could not convert current node into marker node")
/*let marker2 = marker.clone();
Rndr::Placeholder::cast_from(marker).unwrap_or_else(|| {
crate::dom::log("expecting to find a marker. instead, found");
Rndr::log_node(&marker2);
panic!("oops.");
})*/
crate::renderer::types::Placeholder::cast_from(marker.clone())
.unwrap_or_else(|| failed_to_cast_marker_node(marker))
}
}

#[cfg(debug_assertions)]
thread_local! {
static CURRENTLY_HYDRATING: Cell<Option<&'static Location<'static>>> = const { Cell::new(None) };
}

pub(crate) fn set_currently_hydrating(
location: Option<&'static Location<'static>>,
) {
#[cfg(debug_assertions)]
{
CURRENTLY_HYDRATING.set(location);
}
#[cfg(not(debug_assertions))]
{
_ = location;
}
}

pub(crate) fn failed_to_cast_element(tag_name: &str, node: Node) -> Element {
#[cfg(not(debug_assertions))]
{
_ = node;
unreachable!();
}
#[cfg(debug_assertions)]
{
let hydrating = CURRENTLY_HYDRATING
.take()
.map(|n| n.to_string())
.unwrap_or_else(|| "{unknown}".to_string());
web_sys::console::error_3(
&wasm_bindgen::JsValue::from_str(&format!(
"A hydration error occurred while trying to hydrate an \
element defined at {hydrating}.\n\nThe framework expected an \
HTML <{tag_name}> element, but found this instead: ",
)),
&node,
&wasm_bindgen::JsValue::from_str(
"\n\nThe hydration mismatch may have occurred slightly \
earlier, but this is the first time the framework found a \
node of an unexpected type.",
),
);
panic!(
"Unrecoverable hydration error. Please read the error message \
directly above this for more details."
);
}
}

pub(crate) fn failed_to_cast_marker_node(node: Node) -> Comment {
#[cfg(not(debug_assertions))]
{
_ = node;
unreachable!();
}
#[cfg(debug_assertions)]
{
let hydrating = CURRENTLY_HYDRATING
.take()
.map(|n| n.to_string())
.unwrap_or_else(|| "{unknown}".to_string());
web_sys::console::error_3(
&wasm_bindgen::JsValue::from_str(&format!(
"A hydration error occurred while trying to hydrate an \
element defined at {hydrating}.\n\nThe framework expected a \
marker node, but found this instead: ",
)),
&node,
&wasm_bindgen::JsValue::from_str(
"\n\nThe hydration mismatch may have occurred slightly \
earlier, but this is the first time the framework found a \
node of an unexpected type.",
),
);
panic!(
"Unrecoverable hydration error. Please read the error message \
directly above this for more details."
);
}
}

pub(crate) fn failed_to_cast_text_node(node: Node) -> Text {
#[cfg(not(debug_assertions))]
{
_ = node;
unreachable!();
}
#[cfg(debug_assertions)]
{
let hydrating = CURRENTLY_HYDRATING
.take()
.map(|n| n.to_string())
.unwrap_or_else(|| "{unknown}".to_string());
web_sys::console::error_3(
&wasm_bindgen::JsValue::from_str(&format!(
"A hydration error occurred while trying to hydrate an \
element defined at {hydrating}.\n\nThe framework expected a \
text node, but found this instead: ",
)),
&node,
&wasm_bindgen::JsValue::from_str(
"\n\nThe hydration mismatch may have occurred slightly \
earlier, but this is the first time the framework found a \
node of an unexpected type.",
),
);
panic!(
"Unrecoverable hydration error. Please read the error message \
directly above this for more details."
);
}
}
25 changes: 20 additions & 5 deletions tachys/src/mathml/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,17 @@ macro_rules! mathml_global {
At: NextTuple,
<At as NextTuple>::Output<Attr<$crate::html::attribute::[<$attr:camel>], V>>: Attribute,
{
let HtmlElement { tag, children, attributes } = self;
let HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,
children,
attributes
} = self;
HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,

children,
attributes: attributes.next_tuple($crate::html::attribute::$attr(value)),
}
Expand All @@ -46,10 +53,11 @@ macro_rules! mathml_elements {

{
HtmlElement {
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
tag: [<$tag:camel>],
attributes: (),
children: (),

}
}

Expand Down Expand Up @@ -84,10 +92,17 @@ macro_rules! mathml_elements {
At: NextTuple,
<At as NextTuple>::Output<Attr<$crate::html::attribute::[<$attr:camel>], V>>: Attribute,
{
let HtmlElement { tag, children, attributes } = self;
let HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,
children,
attributes
} = self;
HtmlElement {
#[cfg(debug_assertions)]
defined_at,
tag,

children,
attributes: attributes.next_tuple($crate::html::attribute::$attr(value)),
}
Expand Down
Loading

0 comments on commit 3de0414

Please sign in to comment.