Last active
September 10, 2024 13:28
-
-
Save metatoaster/2798451bd9bc99d57b73c46d7cc8ca9a to your computer and use it in GitHub Desktop.
Leptos app demonstrating duplicate/superfluous suspense calls (leptos-rs/leptos#2937)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use leptos::prelude::*; | |
use leptos_meta::{MetaTags, *}; | |
use leptos_router::{ | |
components::{A, Routes, Route, Router, ParentRoute}, | |
hooks::use_params, | |
nested_router::Outlet, | |
params::Params, | |
path, | |
SsrMode, | |
StaticSegment, | |
ParamSegment, | |
WildcardSegment, | |
}; | |
#[derive(Clone, Debug, thiserror::Error, PartialEq, serde::Serialize, serde::Deserialize)] | |
pub enum AppError { | |
#[error("500 Internal Server Error")] | |
InternalServerError, | |
} | |
pub fn shell(options: LeptosOptions) -> impl IntoView { | |
view! { | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"/> | |
<meta name="viewport" content="width=device-width, initial-scale=1"/> | |
<AutoReload options=options.clone()/> | |
<HydrationScripts options/> | |
<MetaTags/> | |
</head> | |
<body> | |
<App/> | |
</body> | |
</html> | |
} | |
} | |
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | |
pub struct Item { | |
id: i64, | |
name: Option<String>, | |
field: Option<String>, | |
} | |
#[server] | |
async fn list_items() -> Result<Vec<i64>, ServerFnError> { | |
// emulate database query overhead | |
tokio::time::sleep(std::time::Duration::from_millis(25)).await; | |
Ok(vec![1, 2, 3, 4]) | |
} | |
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | |
pub struct GetItemResult(pub Item, pub Vec<String>); | |
#[server] | |
async fn get_item(id: i64) -> Result<GetItemResult, ServerFnError> { | |
// emulate database query overhead | |
tokio::time::sleep(std::time::Duration::from_millis(25)).await; | |
let name = None::<String>; | |
let field = None::<String>; | |
Ok(GetItemResult(Item { id, name, field }, ["path1", "path2", "path3"].into_iter() | |
.map(str::to_string) | |
.collect::<Vec<_>>() | |
)) | |
} | |
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | |
pub struct InspectItemResult(pub Item, pub String, pub Vec<String>); | |
#[server] | |
async fn inspect_item(id: i64, path: String) -> Result<InspectItemResult, ServerFnError> { | |
// emulate database query overhead | |
tokio::time::sleep(std::time::Duration::from_millis(25)).await; | |
let mut split = path.split('/'); | |
let name = split.next().map(str::to_string); | |
let path = name.clone() | |
.expect("name should have been defined at this point"); | |
let field = split.next().map(str::to_string); | |
Ok(InspectItemResult(Item { id, name, field }, path, ["field1", "field2", "field3"].into_iter() | |
.map(str::to_string) | |
.collect::<Vec<_>>() | |
)) | |
} | |
#[component] | |
pub fn App() -> impl IntoView { | |
// Provides context that manages stylesheets, titles, meta tags, etc. | |
provide_meta_context(); | |
let ssr = SsrMode::Async; | |
let fallback = || view! { "Page not found." }.into_view(); | |
let count = RwSignal::new(0); | |
provide_context::<RwSignal<i64>>(count); | |
provide_field_nav_portlet_context(); | |
view! { | |
<Stylesheet id="leptos" href="/pkg/staging_ground.css"/> | |
<Title text="Leptos Demo Staging Ground"/> | |
<Meta name="color-scheme" content="dark light"/> | |
<Router> | |
<nav> | |
<A href="/">"Home"</A> | |
<A href="/item/">"Item Listing"</A> | |
<a href="/item/3/">"Target 3##"</a> | |
<a href="/item/4/">"Target 4##"</a> | |
<a href="/item/4/path1/">"Target 41#"</a> | |
<a href="/item/4/path2/">"Target 42#"</a> | |
<a href="/item/1/path2/field3">"Target 123"</a> | |
</nav> | |
<FieldNavPortlet/> | |
<main> | |
<Routes fallback> | |
<Route path=path!("") view=HomePage/> | |
<ParentRoute path=StaticSegment("/item") view=ItemRoot ssr> | |
<Route path=StaticSegment("/") view=ItemListing/> | |
<ParentRoute path=ParamSegment("id") view=ItemTop> | |
<Route path=StaticSegment("/") view=ItemOverview/> | |
<Route path=WildcardSegment("path") view=ItemInspect/> | |
</ParentRoute> | |
</ParentRoute> | |
</Routes> | |
</main> | |
</Router> | |
} | |
} | |
#[component] | |
fn HomePage() -> impl IntoView { | |
expect_context::<WriteSignal<Option<FieldNavCtx>>>().set(None); | |
view! { | |
<Title text="Home Page"/> | |
<h1>"Home Page"</h1> | |
<ul> | |
<li><a href="/item/">"Item Listing"</a></li> | |
<li><a href="/item/4/path1/">"Target 41#"</a></li> | |
</ul> | |
} | |
} | |
#[component] | |
fn ItemRoot() -> impl IntoView { | |
provide_context(Resource::new_blocking( | |
move || (), | |
move |_| async move { | |
list_items().await | |
}, | |
)); | |
view! { | |
<Title text="ItemRoot"/> | |
<h2>"<ItemRoot/>"</h2> | |
<Outlet/> | |
} | |
} | |
#[component] | |
fn ItemListing() -> impl IntoView { | |
expect_context::<WriteSignal<Option<FieldNavCtx>>>().set(None); | |
let resource = expect_context::<Resource<Result<Vec<i64>, ServerFnError>>>(); | |
let item_listing = move || Suspend::new(async move { | |
resource.await.map(|items| items | |
.into_iter() | |
.map(move |item| view! { | |
<li><a href=format!("/item/{item}/")>"Item "{item}</a></li> | |
}) | |
.collect_view() | |
) | |
}); | |
view! { | |
<Title text="ItemListing"/> | |
<h3>"<ItemListing/>"</h3> | |
<ul> | |
<Suspense> | |
{item_listing} | |
</Suspense> | |
</ul> | |
} | |
} | |
#[derive(Params, PartialEq, Clone, Debug)] | |
struct ItemTopParams { | |
id: Option<i64>, | |
} | |
#[component] | |
fn ItemTop() -> impl IntoView { | |
let params = use_params::<ItemTopParams>(); | |
provide_context(Resource::new_blocking( | |
move || params.get().map(|p| p.id), | |
move |id| async move { | |
match id { | |
Err(_) => Err(AppError::InternalServerError), | |
Ok(Some(id)) => get_item(id) | |
.await | |
.map_err(|_| AppError::InternalServerError), | |
_ => Err(AppError::InternalServerError), | |
} | |
}, | |
)); | |
view! { | |
<Title text="ItemTop"/> | |
<h4>"<ItemTop/>"</h4> | |
<Outlet/> | |
} | |
} | |
#[component] | |
fn ItemOverview() -> impl IntoView { | |
expect_context::<WriteSignal<Option<FieldNavCtx>>>().set(None); | |
let resource = expect_context::<Resource<Result<GetItemResult, AppError>>>(); | |
let item_view = move || Suspend::new(async move { | |
resource.await.map(|GetItemResult(item, names)| view! { | |
<Title text=format!("Viewing {item:?}")/> | |
<p>{format!("Viewing {item:?}")}</p> | |
<ul>{ | |
let id = item.id; | |
names.into_iter() | |
.map(|name| view! { | |
<li><a href=format!("/item/{id}/{name}/")>{format!("Inspect {name}")}</a></li> | |
}) | |
.collect_view() | |
}</ul> | |
}) | |
}); | |
view! { | |
<h5>"<ItemOverview/>"</h5> | |
<Suspense> | |
{item_view} | |
</Suspense> | |
} | |
} | |
#[derive(Params, PartialEq, Clone, Debug)] | |
struct ItemInspectParams { | |
path: Option<String>, | |
} | |
#[component] | |
fn ItemInspect() -> impl IntoView { | |
let count = expect_context::<RwSignal<i64>>(); | |
let params = use_params::<ItemInspectParams>(); | |
let res_overview = expect_context::<Resource<Result<GetItemResult, AppError>>>(); | |
let res_inspect = Resource::new_blocking( | |
move || params.get().map(|p| p.path), | |
move |p| async move { | |
leptos::logging::log!("res_inspect: res_overview.await"); | |
let overview = res_overview.await; | |
leptos::logging::log!("res_inspect: resolved res_overview.await"); | |
let result = match (overview, p) { | |
(Ok(item), Ok(Some(path))) => { | |
leptos::logging::log!("res_inspect: inspect_item().await"); | |
inspect_item(item.0.id, path.clone()) | |
.await | |
.map_err(|_| AppError::InternalServerError) | |
} | |
_ => Err(AppError::InternalServerError), | |
}; | |
leptos::logging::log!("res_inspect: resolved inspect_item().await"); | |
result | |
} | |
); | |
let inspect_view = move || { | |
leptos::logging::log!("inspect_view closure invoked"); | |
Suspend::new(async move { | |
leptos::logging::log!("inspect_view Suspend::new() called"); | |
let result = res_inspect.await.map(|InspectItemResult(item, name, fields)| { | |
leptos::logging::log!("inspect_view res_inspect awaited"); | |
let id = item.id; | |
expect_context::<WriteSignal<Option<FieldNavCtx>>>().set(Some( | |
fields.iter() | |
.map(|field| FieldNavItem { | |
href: format!("/item/{id}/{name}/{field}"), | |
text: format!("{field}"), | |
}) | |
.collect::<Vec<_>>() | |
.into() | |
)); | |
view! { | |
<Title text=format!("Inspecting {item:?}")/> | |
<p>{format!("Inspecting {item:?}")}</p> | |
<ul>{ | |
fields.iter() | |
.map(|field| view! { | |
<li><a href=format!("/item/{id}/{name}/{field}")>{ | |
format!("Inspect {name}/{field}") | |
}</a></li> | |
}) | |
.collect_view() | |
}</ul> | |
} | |
}); | |
count.update_untracked(|x| *x += 1); | |
leptos::logging::log!( | |
"returning result, result.is_ok() = {}, count = {}", | |
result.is_ok(), | |
count.get(), | |
); | |
result | |
}) | |
}; | |
view! { | |
<h5>"<ItemInspect/>"</h5> | |
<Suspense> | |
{inspect_view} | |
</Suspense> | |
} | |
} | |
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)] | |
pub struct FieldNavItem { | |
pub href: String, | |
pub text: String, | |
} | |
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)] | |
pub struct FieldNavCtx(pub Option<Vec<FieldNavItem>>); | |
impl From<Vec<FieldNavItem>> for FieldNavCtx { | |
fn from(item: Vec<FieldNavItem>) -> Self { | |
Self(Some(item)) | |
} | |
} | |
#[component] | |
pub fn FieldNavPortlet() -> impl IntoView { | |
let ctx = expect_context::<ReadSignal<Option<FieldNavCtx>>>(); | |
move || { | |
let ctx = ctx.get(); | |
ctx.map(|ctx| view! { | |
<div id="FieldNavPortlet"> | |
<span>"FieldNavPortlet:"</span> | |
<nav>{ | |
ctx.0.map(|ctx| { | |
ctx.into_iter() | |
.map(|FieldNavItem { href, text }| { | |
view! { | |
<A href=href>{text}</A> | |
} | |
}) | |
.collect_view() | |
}) | |
}</nav> | |
</div> | |
}) | |
} | |
} | |
pub fn provide_field_nav_portlet_context() { | |
// wrapping the Ctx in an Option allows better ergonomics whenever it isn't needed | |
let (ctx, set_ctx) = signal(None::<FieldNavCtx>); | |
provide_context(ctx); | |
provide_context(set_ctx); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
font-family: sans-serif; | |
} | |
body > nav { | |
padding: 0.5em 0; | |
} | |
body > nav > a { | |
padding: 0.5em; | |
border: 1px transparent solid; | |
} | |
body > nav > a[aria-current] { | |
border: 1px #808080 solid; | |
} | |
div#FieldNavPortlet { | |
padding: 0.5em 0; | |
} | |
div#FieldNavPortlet > nav { | |
display: inline; | |
} | |
div#FieldNavPortlet > nav > a { | |
padding: 0.2em; | |
border: 1px transparent solid; | |
} | |
div#FieldNavPortlet > nav > a[aria-current] { | |
border: 1px #808080 solid; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment