From e7969c8050d62378e0a81d70971f3a9b2c817d8d Mon Sep 17 00:00:00 2001 From: Myrddin Dundragon Date: Sat, 27 Sep 2025 13:21:56 -0400 Subject: [PATCH] Fixed the TagSelector reactivity. use_server_future was not reliably re-running when url_tag changed during direct URL navigation, causing tags to remain unselected. use_resource provides consistent reactivity across all navigation methods and handles both data fetching and selection logic atomically. --- bard/src/components/list.rs | 205 ++++++++++++++++++++++++++---------- bard/src/components/post.rs | 61 +++++------ bard/src/components/tags.rs | 11 +- bard/src/pages/blog.rs | 30 +----- 4 files changed, 183 insertions(+), 124 deletions(-) diff --git a/bard/src/components/list.rs b/bard/src/components/list.rs index 4817b2d..8ffa868 100644 --- a/bard/src/components/list.rs +++ b/bard/src/components/list.rs @@ -30,18 +30,20 @@ pub fn BlogItem(title: String, slug: String, author: String, summary: String, tags: Vec) -> Element { - println!("Blog Item: {title} -- [{tags:?}]"); rsx! { - li + article { - key: "{slug}", class: "blog_item", - Link + h2 { - to: Page::Post { slug: slug.clone() }, - h1 { "{title}" } + Link + { + to: Page::Post { slug: slug.clone() }, + + "{title}" + } } TagList @@ -55,7 +57,7 @@ pub fn BlogItem(title: String, slug: String, author: String, summary: String, } } - h4 { "Author: {author}" } + p { b { "Author: {author}" } } p { "{summary}" } } @@ -66,8 +68,10 @@ pub fn BlogItem(title: String, slug: String, author: String, summary: String, #[component] pub fn BlogList(tags: Signal>, children: Element) -> Element { - let list = use_server_future(move || { - let categories = tags().iter().cloned().collect(); + let list = use_server_future(move || + { + let t = tags(); + let categories = t.iter().cloned().collect(); async move { get_blog_list(categories).await } })?; @@ -76,41 +80,40 @@ pub fn BlogList(tags: Signal>, children: Element) -> Element { section { - class: "blog_list_style", - ul - { - class: "blog_list", + class: "blog_list", - if let Some(Ok(lores)) = &*list.read() + h1 { class: "blog_title", "Runes & Ramblings" } + + if let Some(Ok(lores)) = &*list.read() + { + for lore in lores { - for lore in lores + BlogItem { - BlogItem - { - title: lore.title.clone(), - slug: lore.slug.clone(), - author: lore.author.clone(), - summary: lore.summary.clone(), - tags: lore.tags.clone() - } + title: lore.title.clone(), + slug: lore.slug.clone(), + author: lore.author.clone(), + summary: lore.summary.clone(), + tags: lore.tags.clone() } } - else if let Some(Err(e)) = &*list.read() - { - p { "Unable to show post header." } - p { "{e}" } - } - else - { - p { "Loading..." } - } - - {children} } + else if let Some(Err(e)) = &*list.read() + { + p { "Unable to show post header." } + p { "{e}" } + } + else + { + p { "Loading..." } + } + + {children} } } } + #[component] pub fn ToggleTag(tag: String, toggled_tags: Signal>) -> Element { @@ -130,65 +133,151 @@ pub fn ToggleTag(tag: String, toggled_tags: Signal>) -> Element }, } - span { "{tag}" } + "{tag}" } } } + +// Using use_resource instead of use_server_future for URL-dependent tag selection +// +// While use_server_future should theoretically be reactive when reading signals in the +// closure (per Dioxus docs), in practice it doesn't reliably re-run when the url_tag +// signal changes, especially during direct URL navigation (typing URLs in browser). +// +// use_resource provides more reliable reactivity for this use case because: +// 1. It explicitly depends on url_tag and consistently re-runs when it changes +// 2. It handles both async data fetching AND selection logic in a single atomic operation +// 3. It works consistently across all navigation methods (links, direct URLs, etc.) +// 4. It avoids timing coordination issues between separate hooks +// +// This approach combines fetching available tags from the server with determining +// which tags should be selected based on the current URL, returning both pieces +// of data together for clean state management. #[component] -pub fn TagSelector(show_all: Signal, toggled_tags: Signal>) -> Element +pub fn TagSelector(url_tag: ReadOnlySignal, + toggled_tags: Signal>) -> Element { - let toggle_all: bool = show_all(); - - println!("Tag Selector toggled tags: {:?}", toggled_tags()); - let tags_future = use_server_future(move || async move { get_tags().await })?; - - use_effect(move || + // Use use_resource to handle both fetching tags AND initializing selection + let tags_and_selection = use_resource(move || { - if let Some(Ok(tags)) = &*tags_future.read() + let current_url = url_tag(); + + async move { - if toggle_all + match get_tags().await { - for tag in tags + Ok(available_tags) => { - toggled_tags.write().insert(tag.clone()); + // Determine what should be selected based on URL + let should_show_all = current_url.is_empty() || current_url == "all"; + + let selected_tags: HashSet = if should_show_all + { + available_tags.iter().cloned().collect() + } + else + { + let url_categories = convert_categories(¤t_url); + + let filtered: HashSet = url_categories.into_iter() + .filter(|tag| available_tags.contains(tag)) + .collect(); + + filtered + }; + + Ok((available_tags, selected_tags)) + } + + Err(e) => + { + eprintln!("RESOURCE - Error: {}", e); + Err(e) } } } }); + // Separate effect to update toggled_tags when resource completes + // This separates reading the resource from writing to toggled_tags + use_effect(move || + { + if let Some(Ok((_, selected_tags))) = &*tags_and_selection.read() + { + toggled_tags.set(selected_tags.clone()); + } + }); + + rsx! { section { - class: "blog_nav_style", - div + class: "blog_nav", + + h2 { class: "visually_hidden", "Filters and Navigation" } + + fieldset { class: "tag_style", - h2 { "Categories" } + + legend { "Category Filter" } + ul { class: "tag_list", - if let Some(Ok(tags)) = tags_future() + match &*tags_and_selection.read() { - for tag in tags + Some(Ok((available_tags, _))) => { - li + rsx! { - key: "selector-{tag}", - class: "tag_item", - - ToggleTag + for tag in available_tags { - tag: &tag, - toggled_tags: toggled_tags + li + { + key: "selector-{tag}", + class: "tag_item", + + ToggleTag + { + tag: tag.clone(), + toggled_tags: toggled_tags + } + } } } } + + Some(Err(_)) => + { + rsx! + { + li { "Error loading tags" } + } + } + + None => + { + rsx! + { + li { "Loading tags..." } + } + } } } } } } } + +fn convert_categories(categories: &str) -> HashSet +{ + categories + .split('+') + .filter(|s| !s.is_empty() && *s != "all") + .map(str::to_string) + .collect() +} diff --git a/bard/src/components/post.rs b/bard/src/components/post.rs index dce16ae..5161630 100644 --- a/bard/src/components/post.rs +++ b/bard/src/components/post.rs @@ -30,10 +30,13 @@ pub fn PostHeaderAuthor(adventurer: Adventurer) -> Element { rsx! { - h4 + p { - "Author: ", - a { href: "{adventurer.legend.profile}", "{adventurer.name} @{adventurer.handle}" } + b + { + "Author: ", + a { href: "{adventurer.legend.profile}", "{adventurer.name} @{adventurer.handle}" } + } } } } @@ -99,7 +102,7 @@ pub fn BlogPost(slug: Signal, children: Element) -> Element { article { - class: "blog_post_style", + class: "blog_post", if let Some(Ok(tale)) = (post_future.value())() { @@ -137,42 +140,36 @@ pub fn TagNav() -> Element { section { - class: "blog_nav_style", - div + class: "blog_nav", + + fieldset { - class: "tag_style", - h2 { "Categories" } - ul + class: "tag_list", + + legend { "Category Filter" } + + if let Some(Ok(categories)) = &*tags.read() { - class: "tag_list", - if let Some(Ok(categories)) = &*tags.read() + for tag in categories { - for tag in categories - { - li + Link { - key: "{tag}", - class: "tag_item", - Link - { - to: Page::Blog { tag: tag.clone() }, + class: "tag_item tag_{tag}", - "{tag}" - } + to: Page::Blog { tag: tag.clone() }, - //a { href: "/blog/{tag}", "{tag}" } + "{tag}" } } - } - else if let Some(Err(e)) = &*tags.read() - { - p { "Unable to show desired post." } - p { "{e}" } - } - else - { - p { "Loading..." } - } + } + else if let Some(Err(e)) = &*tags.read() + { + p { "Unable to load tags." } + p { "{e}" } + } + else + { + p { "Loading..." } } } } diff --git a/bard/src/components/tags.rs b/bard/src/components/tags.rs index 8d75385..5b19274 100644 --- a/bard/src/components/tags.rs +++ b/bard/src/components/tags.rs @@ -37,15 +37,10 @@ pub fn TagList(children: Element) -> Element { rsx! { - h5 + ul { - class: "blog_tag_style", - - ul - { - class: "tag_list", - {children} - } + class: "blog_tag tag_list", + {children} } } } diff --git a/bard/src/pages/blog.rs b/bard/src/pages/blog.rs index 5edad78..30f25a0 100644 --- a/bard/src/pages/blog.rs +++ b/bard/src/pages/blog.rs @@ -7,34 +7,12 @@ use crate::page::Page; -fn convert_categories(categories: &str) -> HashSet -{ - categories - .split('+') - .filter(|s| !s.is_empty() && *s != "all") - .map(str::to_string) - .collect() -} - - - /// Blog page #[component] pub fn Blog(tag: ReadOnlySignal) -> Element { - let mut show_all: Signal = - use_signal(|| tag().is_empty() || tag() == "all"); - let mut categories: Signal> = - use_signal(|| convert_categories(&tag())); - - use_effect(move || - { - let new_tags = convert_categories(&tag()); - categories.set(new_tags); - }); - - println!("Blog Categories: {:?}", categories()); + use_signal(|| HashSet::new()); rsx! { @@ -47,13 +25,13 @@ pub fn Blog(tag: ReadOnlySignal) -> Element BlogList { - tags: categories.clone() + tags: categories } TagSelector { - show_all: show_all.clone(), - toggled_tags: categories.clone() + url_tag: tag, + toggled_tags: categories } } }