use leptos::prelude::*;
use serde::{Serialize, de::DeserializeOwned};
use std::future::Future;
pub(crate) fn type_hydration_id<T: 'static>() -> String {
std::any::type_name::<T>()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '_' })
.collect()
}
#[cfg(feature = "ssr")]
pub(crate) fn serialize_for_injection<T: Serialize>(value: &T) -> String {
serde_json::to_string(value).unwrap_or_default()
}
#[cfg(all(not(feature = "ssr"), feature = "hydrate"))]
fn read_injected_state<T: DeserializeOwned>(id: &str) -> Option<T> {
use js_sys::JSON;
use wasm_bindgen::JsCast as _;
use wasm_bindgen::JsValue;
let doc = document();
let script_id = format!("__lh_{}", id);
let el: JsValue = js_sys::Reflect::get(&doc, &JsValue::from_str("getElementById"))
.ok()
.and_then(|f| f.dyn_into::<js_sys::Function>().ok())
.and_then(|f| f.call1(&doc, &JsValue::from_str(&script_id)).ok())
.filter(|v: &JsValue| !v.is_null() && !v.is_undefined())?;
let text = js_sys::Reflect::get(&el, &JsValue::from_str("textContent"))
.ok()
.and_then(|v| v.as_string())?;
let js_val = JSON::parse(&text).ok()?;
serde_wasm_bindgen::from_value(js_val).ok()
}
#[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))]
fn read_injected_state<T: DeserializeOwned>(_id: &str) -> Option<T> {
None
}
pub trait Hydratable:
Clone + Serialize + DeserializeOwned + Default + Send + Sync + 'static
{
fn initial() -> Self;
fn fetch() -> impl Future<Output = Option<Self>> + Send + 'static {
async { None }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HydratedSignal<T: 'static>(pub RwSignal<T>);
pub fn use_hydrate_signal<T, Fut>(
ssr_value: impl Fn() -> T + 'static,
fetcher: impl Fn() -> Fut + Send + Sync + 'static,
) -> (RwSignal<T>, LocalResource<Option<T>>)
where
T: Clone + Serialize + DeserializeOwned + Default + Send + Sync + PartialEq + 'static,
Fut: Future<Output = Option<T>> + Send + 'static,
{
let initial_val = ssr_value();
let signal = RwSignal::new(initial_val);
let resource = LocalResource::new(move || {
let f = fetcher();
async move {
f.await
}
});
#[cfg(not(feature = "ssr"))]
{
leptos::task::spawn_local(async move {
if let Some(val) = resource.await {
signal.set(val);
}
});
}
(signal, resource)
}
#[cfg(all(not(feature = "ssr"), feature = "hydrate"))]
fn read_raw_injected_state(id: &str) -> Option<String> {
let script_id = format!("__lh_{}", id);
document()
.get_element_by_id(&script_id)
.and_then(|el| el.text_content())
}
#[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))]
fn read_raw_injected_state(_id: &str) -> Option<String> {
None
}
pub fn get_cookie(name: &str) -> Option<String> {
#[cfg(feature = "ssr")]
{
use http::header::COOKIE;
use http::request::Parts;
use leptos::prelude::use_context;
use_context::<Parts>().and_then(|parts| {
parts
.headers
.get(COOKIE)
.and_then(|h| h.to_str().ok())
.and_then(|cookies| {
cookies.split("; ").find_map(|s| {
let mut parts = s.splitn(2, '=');
let k = parts.next()?.trim();
let v = parts.next()?.trim();
if k == name { Some(v.to_string()) } else { None }
})
})
})
}
#[cfg(all(not(feature = "ssr"), feature = "hydrate"))]
{
let cookies = js_sys::Reflect::get(&document(), &wasm_bindgen::JsValue::from_str("cookie"))
.ok()
.and_then(|v| v.as_string())
.unwrap_or_default();
cookies.split("; ").find_map(|s: &str| {
let mut parts = s.splitn(2, '=');
let k = parts.next()?.trim();
let v = parts.next()?.trim();
if k == name { Some(v.to_string()) } else { None }
})
}
#[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))]
{
let _ = name;
None
}
}
pub fn get_query_param(name: &str) -> Option<String> {
#[cfg(feature = "ssr")]
{
use http::request::Parts;
use leptos::prelude::use_context;
use_context::<Parts>().and_then(|parts| {
parts.uri.query().and_then(|q| {
q.split('&').find_map(|s| {
let mut parts = s.splitn(2, '=');
let k = parts.next()?.trim();
let v = parts.next()?.trim();
if k == name { Some(v.to_string()) } else { None }
})
})
})
}
#[cfg(all(not(feature = "ssr"), feature = "hydrate"))]
{
window().location().search().ok().and_then(|search| {
if search.is_empty() {
return None;
}
let query = search.trim_start_matches('?');
query.split('&').find_map(|s: &str| {
let mut parts = s.splitn(2, '=');
let k = parts.next()?.trim();
let v = parts.next()?.trim();
if k == name { Some(v.to_string()) } else { None }
})
})
}
#[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))]
{
let _ = name;
None
}
}
pub fn set_cookie(name: &str, value: &str, options: &str) {
#[cfg(feature = "ssr")]
{
use http::HeaderValue;
use http::header::SET_COOKIE;
use leptos::prelude::use_context;
use leptos_axum::ResponseOptions;
if let Some(res) = use_context::<ResponseOptions>() {
let cookie = format!("{}={}{}", name, value, options);
if let Ok(val) = HeaderValue::from_str(&cookie) {
res.insert_header(SET_COOKIE, val);
}
}
}
#[cfg(all(not(feature = "ssr"), feature = "hydrate"))]
{
let cookie = format!("{}={}{}", name, value, options);
let _ = js_sys::Reflect::set(
&document(),
&wasm_bindgen::JsValue::from_str("cookie"),
&wasm_bindgen::JsValue::from_str(&cookie),
);
}
#[cfg(all(not(feature = "ssr"), not(feature = "hydrate")))]
{
let _ = (name, value, options);
}
}
pub fn get_header(name: &str) -> Option<String> {
#[cfg(feature = "ssr")]
{
use http::request::Parts;
use leptos::prelude::use_context;
use_context::<Parts>().and_then(|parts| {
parts
.headers
.get(name)
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string())
})
}
#[cfg(not(feature = "ssr"))]
{
let _ = name;
None
}
}
pub fn set_header(name: &str, value: &str) {
#[cfg(feature = "ssr")]
{
use http::HeaderValue;
use http::header::HeaderName;
use leptos::prelude::use_context;
use leptos_axum::ResponseOptions;
use std::str::FromStr;
if let Some(res) = use_context::<ResponseOptions>() {
if let (Ok(name), Ok(val)) = (HeaderName::from_str(name), HeaderValue::from_str(value)) {
res.insert_header(name, val);
}
}
}
#[cfg(not(feature = "ssr"))]
{
let _ = (name, value);
}
}
pub fn get_referer_query_param(name: &str) -> Option<String> {
#[cfg(feature = "ssr")]
{
use http::header::REFERER;
use http::request::Parts;
use leptos::prelude::use_context;
use_context::<Parts>().and_then(|parts| {
parts
.headers
.get(REFERER)
.and_then(|h| h.to_str().ok())
.and_then(|referer| {
let query = referer.split('?').nth(1)?;
query.split('&').find_map(|s| {
let mut p = s.splitn(2, '=');
let k = p.next()?.trim();
let v = p.next()?.trim();
if k == name { Some(v.to_string()) } else { None }
})
})
})
}
#[cfg(not(feature = "ssr"))]
{
let _ = name;
None
}
}
#[component]
pub fn HydrateState<T>(#[prop(optional)] marker: std::marker::PhantomData<T>) -> impl IntoView
where
T: Hydratable + PartialEq,
{
let _ = marker;
let id = type_hydration_id::<T>();
let script_id = format!("__lh_{}", id);
#[cfg(feature = "ssr")]
let initial_val = T::initial();
#[cfg(not(feature = "ssr"))]
let initial_val = read_injected_state::<T>(&id).unwrap_or_else(T::initial);
#[cfg(feature = "ssr")]
let json = serialize_for_injection(&initial_val);
#[cfg(not(feature = "ssr"))]
let json = read_raw_injected_state(&id).unwrap_or_default();
let cloned = initial_val.clone();
let (signal, resource) = use_hydrate_signal(move || cloned.clone(), || T::fetch());
provide_context(HydratedSignal(signal));
provide_context(resource);
view! {
<script type="application/json" id={script_id} inner_html={json} />
}
}
#[component]
pub fn HydrateContext<T>(
children: Children,
#[prop(optional)] marker: std::marker::PhantomData<T>,
) -> impl IntoView
where
T: Hydratable + PartialEq,
{
let _ = marker;
let id = type_hydration_id::<T>();
let script_id = format!("__lh_{}", id);
#[cfg(feature = "ssr")]
let initial_val = T::initial();
#[cfg(not(feature = "ssr"))]
let initial_val = read_injected_state::<T>(&id).unwrap_or_else(T::initial);
#[cfg(feature = "ssr")]
let json = serialize_for_injection(&initial_val);
#[cfg(not(feature = "ssr"))]
let json = read_raw_injected_state(&id).unwrap_or_default();
let cloned = initial_val.clone();
let (signal, resource) = use_hydrate_signal(move || cloned.clone(), || T::fetch());
provide_context(HydratedSignal(signal));
provide_context(resource);
view! {
{children()}
<script type="application/json" id={script_id} inner_html={json} />
}
}
#[component]
pub fn HydrateStateWith<T, Fut>(
ssr_value: impl Fn() -> T + 'static,
fetcher: impl Fn() -> Fut + Send + Sync + 'static,
#[prop(optional)]
server_value: Option<T>,
) -> impl IntoView
where
T: Clone + Serialize + DeserializeOwned + Default + Send + Sync + PartialEq + 'static,
Fut: Future<Output = Option<T>> + Send + 'static,
{
let id = type_hydration_id::<T>();
let script_id = format!("__lh_{}", id);
#[cfg(feature = "ssr")]
let (initial_val, json) = {
let val = server_value.unwrap_or_else(&ssr_value);
let json = serialize_for_injection(&val);
(val, json)
};
#[cfg(not(feature = "ssr"))]
let (initial_val, json) = {
let _ = server_value;
let val = read_injected_state::<T>(&id).unwrap_or_else(ssr_value);
let json = read_raw_injected_state(&id).unwrap_or_default();
(val, json)
};
let cloned = initial_val.clone();
let (signal, resource) = use_hydrate_signal(move || cloned.clone(), fetcher);
provide_context(HydratedSignal(signal));
provide_context(resource);
view! {
<script type="application/json" id={script_id} inner_html={json} />
}
}
#[component]
pub fn HydrateContextWith<T, Fut>(
ssr_value: impl Fn() -> T + 'static,
fetcher: impl Fn() -> Fut + Send + Sync + 'static,
children: Children,
#[prop(optional)] server_value: Option<T>,
) -> impl IntoView
where
T: Clone + Serialize + DeserializeOwned + Default + Send + Sync + PartialEq + 'static,
Fut: Future<Output = Option<T>> + Send + 'static,
{
let id = type_hydration_id::<T>();
let script_id = format!("__lh_{}", id);
#[cfg(feature = "ssr")]
let (initial_val, json) = {
let val = server_value.unwrap_or_else(&ssr_value);
let json = serialize_for_injection(&val);
(val, json)
};
#[cfg(not(feature = "ssr"))]
let (initial_val, json) = {
let _ = server_value;
let val = read_injected_state::<T>(&id).unwrap_or_else(ssr_value);
let json = read_raw_injected_state(&id).unwrap_or_default();
(val, json)
};
let cloned = initial_val.clone();
let (signal, resource) = use_hydrate_signal(move || cloned.clone(), fetcher);
provide_context(HydratedSignal(signal));
provide_context(resource);
view! {
{children()}
<script type="application/json" id={script_id} inner_html={json} />
}
}
pub fn use_hydrated<T>() -> RwSignal<T>
where
T: Clone + Send + Sync + 'static,
{
use_context::<HydratedSignal<T>>().map(|s| s.0).expect(
&format!(
"HydratedSignal<{}> not found. Did you wrap this part of the tree in <HydrateState<{0}> />, <HydrateContext<{0}> />, <HydrateStateWith<{0}> />, or <HydrateContextWith<{0}> />?",
std::any::type_name::<T>()
)
)
}
pub fn try_use_hydrated<T>() -> Option<RwSignal<T>>
where
T: Clone + Send + Sync + 'static,
{
use_context::<HydratedSignal<T>>().map(|s| s.0)
}
pub fn use_hydrated_resource<T>() -> LocalResource<Option<T>>
where
T: Clone + Send + Sync + 'static,
{
use_context::<LocalResource<Option<T>>>().unwrap_or_else(|| {
panic!(
"Hydrated Resource<{}> not found. Did you wrap this part of the tree in <HydrateState<{0}> />, <HydrateContext<{0}> />, <HydrateStateWith<{0}> />, or <HydrateContextWith<{0}> />?",
std::any::type_name::<T>()
)
})
}
pub fn try_use_hydrated_resource<T>() -> Option<LocalResource<Option<T>>>
where
T: Clone + Send + Sync + 'static,
{
use_context::<LocalResource<Option<T>>>()
}
#[cfg(test)]
mod tests;