Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
e7cfe788b2 feat: <Provider/> component to fix context shadowing (closes #2038) 2023-11-18 08:23:45 -05:00
5 changed files with 92 additions and 22 deletions

View File

@@ -192,9 +192,11 @@ mod error_boundary;
pub use error_boundary::*;
mod animated_show;
mod for_loop;
mod provider;
mod show;
pub use animated_show::*;
pub use for_loop::*;
pub use provider::*;
#[cfg(feature = "experimental-islands")]
pub use serde;
#[cfg(feature = "experimental-islands")]

40
leptos/src/provider.rs Normal file
View File

@@ -0,0 +1,40 @@
use leptos::*;
#[component]
/// Uses the context API to [`provide_context`] to its children and descendants,
/// without overwriting any contexts of the same type in its own reactive scope.
///
/// This prevents issues related to “context shadowing.”
///
/// ```rust
/// # use leptos::*;
/// #[component]
/// pub fn App() -> impl IntoView {
/// // each Provider will only provide the value to its children
/// view! {
/// <Provider value=1u8>
/// // correctly gets 1 from context
/// {use_context::<u8>().unwrap_or(0)}
/// </Provider>
/// <Provider value=2u8>
/// // correctly gets 2 from context
/// {use_context::<u8>().unwrap_or(0)}
/// </Provider>
/// // does not find any u8 in context
/// {use_context::<u8>().unwrap_or(0)}
/// }
/// }
/// ```
pub fn Provider<T>(
/// The value to be provided via context.
value: T,
children: Children,
) -> impl IntoView
where
T: Clone + 'static,
{
run_as_child(move || {
provide_context(value);
children()
})
}

View File

@@ -1,3 +1,5 @@
use wasm_bindgen::UnwrapThrowExt;
#[macro_export]
/// Use for tracing property
macro_rules! tracing_props {
@@ -9,9 +11,8 @@ macro_rules! tracing_props {
);
};
($($prop:tt),+ $(,)?) => {
#[cfg(any(debug_assertions, feature = "ssr"))]
{
use ::leptos::leptos_dom::tracing_property::{Match, DebugMatch, DefaultMatch};
use ::leptos::leptos_dom::tracing_property::{Match, SerializeMatch, DefaultMatch};
let mut props = String::from('[');
$(
let prop = (&&Match {
@@ -39,17 +40,29 @@ pub struct Match<T> {
pub value: std::cell::Cell<Option<T>>,
}
pub trait DebugMatch {
pub trait SerializeMatch {
type Return;
fn spez(&self) -> Self::Return;
}
impl<T: core::fmt::Debug> DebugMatch for &Match<&T> {
impl<T: serde::Serialize> SerializeMatch for &Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
let debug_value =
format!("{:?}", self.value.get().unwrap()).replace('"', r#"\""#);
format!(r#"{{"name": "{name}", "value": "{debug_value}"}}"#,)
// suppresses warnings when serializing signals into props
#[cfg(debug_assertions)]
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
let value = serde_json::to_string(self.value.get().unwrap_throw())
.map_or_else(
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
);
#[cfg(debug_assertions)]
leptos_reactive::SpecialNonReactiveZone::exit(prev);
value
}
}
@@ -61,9 +74,7 @@ impl<T> DefaultMatch for Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
format!(
r#"{{"name": "{name}", "value": "[value does not implement Debug]"}}"#
)
format!(r#"{{"name": "{name}", "value": "[unserializable value]"}}"#)
}
}
@@ -76,7 +87,7 @@ fn match_primitive() {
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": "\"string\""}"#);
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
// &str
let test = "string";
@@ -85,7 +96,7 @@ fn match_primitive() {
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": "\"string\""}"#);
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
// u128
let test: u128 = 1;
@@ -127,7 +138,7 @@ fn match_primitive() {
#[test]
fn match_serialize() {
use serde::Serialize;
#[derive(Debug)]
#[derive(Serialize)]
struct CustomStruct {
field: &'static str,
}
@@ -138,10 +149,7 @@ fn match_serialize() {
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(
prop,
r#"{"name": "test", "value": "CustomStruct { field: \"field\" }"}"#
);
assert_eq!(prop, r#"{"name": "test", "value": {"field":"field"}}"#);
// Verification of ownership
assert_eq!(test.field, "field");
}
@@ -162,7 +170,7 @@ fn match_no_serialize() {
.spez();
assert_eq!(
prop,
r#"{"name": "test", "value": "[value does not implement Debug]"}"#
r#"{"name": "test", "value": "[unserializable value]"}"#
);
// Verification of ownership
assert_eq!(test.field, "field");

View File

@@ -84,7 +84,29 @@ use std::any::{Any, TypeId};
/// that was provided in `<Parent/>`, meaning that the second `<Child/>` receives the context
/// from its sibling instead.
///
/// This can be solved by introducing some additional reactivity. In this case, its simplest
/// ### Solution
///
/// If you are using the full Leptos framework, you can use the [`Provider`](leptos::Provider)
/// component to solve this issue.
///
/// ```rust
/// # use leptos::*;
/// #[component]
/// fn Child() -> impl IntoView {
/// let context = expect_context::<&'static str>();
/// // creates a new reactive node, which means the context will
/// // only be provided to its children, not modified in the parent
/// view! {
/// <Provider value="child_context">
/// <div>{format!("child (context: {context})")}</div>
/// </Provider>
/// }
/// }
/// ```
///
/// ### Alternate Solution
///
/// This can also be solved by introducing some additional reactivity. In this case, its simplest
/// to simply make the body of `<Child/>` a function, which means it will be wrapped in a
/// new reactive node when rendered:
/// ```rust

View File

@@ -1247,9 +1247,7 @@ where
}
#[cfg(all(feature = "hydrate", debug_assertions))]
{
if self.serializable != ResourceSerialization::Local
&& !SpecialNonReactiveZone::is_inside()
{
if self.serializable != ResourceSerialization::Local {
crate::macros::debug_warn!(
"At {location}, you are reading a resource in \
`hydrate` mode outside a <Suspense/> or \