Skip to content

Instantly share code, notes, and snippets.

@metatoaster
Last active September 10, 2024 13:28
Show Gist options
  • Save metatoaster/2798451bd9bc99d57b73c46d7cc8ca9a to your computer and use it in GitHub Desktop.
Save metatoaster/2798451bd9bc99d57b73c46d7cc8ca9a to your computer and use it in GitHub Desktop.
Leptos app demonstrating duplicate/superfluous suspense calls (leptos-rs/leptos#2937)
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);
}
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