From 0222182286a5a751d0f3d8107c5db1c44fde8a52 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sun, 11 Aug 2024 17:20:25 -0400 Subject: [PATCH] fix: provide parent params to children (closes #2719) --- router/src/flat_router.rs | 18 ++++++++------- router/src/hooks.rs | 6 ++--- router/src/nested_router.rs | 46 +++++++++++++++++++++++++++---------- router/src/params.rs | 25 +++++++++++++++++++- 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/router/src/flat_router.rs b/router/src/flat_router.rs index d4ac4f7a5..99aa72a9c 100644 --- a/router/src/flat_router.rs +++ b/router/src/flat_router.rs @@ -9,7 +9,7 @@ use any_spawner::Executor; use either_of::{Either, EitherOf3}; use futures::FutureExt; use reactive_graph::{ - computed::ScopedFuture, + computed::{ArcMemo, ScopedFuture}, owner::{provide_context, Owner}, signal::ArcRwSignal, traits::{ReadUntracked, Set}, @@ -202,6 +202,7 @@ where .map(|n| n.to_params().into_iter().collect()) .unwrap_or_default(), ); + let params_memo = ArcMemo::from(params.clone()); match new_match { None => Rc::new(RefCell::new(FlatRoutesViewState { @@ -224,10 +225,9 @@ where let mut view = Box::pin(owner.with(|| { ScopedFuture::new({ - let params = params.clone(); let url = url.clone(); async move { - provide_context(params); + provide_context(params_memo); provide_context(url); view.choose().await } @@ -322,6 +322,7 @@ where let owner = outer_owner.child(); let url = ArcRwSignal::new(url_snapshot.to_owned()); let params = ArcRwSignal::new(matched_params); + let params_memo = ArcMemo::from(params.clone()); let old_owner = mem::replace(&mut initial_state.owner, owner.clone()); let old_url = mem::replace(&mut initial_state.url, url.clone()); let old_params = @@ -336,7 +337,7 @@ where None => { owner.with(|| { provide_context(url); - provide_context(params); + provide_context(params_memo); EitherOf3::B(fallback()) .rebuild(&mut state.borrow_mut().view) }); @@ -358,7 +359,7 @@ where let state = Rc::clone(state); async move { provide_context(url); - provide_context(params); + provide_context(params_memo); let view = if let Some(set_is_routing) = set_is_routing { set_is_routing.set(true); @@ -439,6 +440,7 @@ where .map(|n| n.to_params().into_iter().collect::()) .unwrap_or_default(), ); + let params_memo = ArcMemo::from(params.clone()); let view = match new_match { None => Either::Left((self.fallback)()), Some(matched) => { @@ -447,7 +449,7 @@ where .with(|| { ScopedFuture::new(async move { provide_context(url); - provide_context(params); + provide_context(params_memo); view.choose().await }) }) @@ -594,6 +596,7 @@ where .map(|n| n.to_params().into_iter().collect()) .unwrap_or_default(), ); + let params_memo = ArcMemo::from(params.clone()); match new_match { None => Rc::new(RefCell::new(FlatRoutesViewState { @@ -617,10 +620,9 @@ where let mut view = Box::pin(owner.with(|| { ScopedFuture::new({ - let params = params.clone(); let url = url.clone(); async move { - provide_context(params); + provide_context(params_memo); provide_context(url); view.choose().await } diff --git a/router/src/hooks.rs b/router/src/hooks.rs index c5783086b..320afd431 100644 --- a/router/src/hooks.rs +++ b/router/src/hooks.rs @@ -156,7 +156,7 @@ pub fn use_location() -> Location { } #[track_caller] -fn use_params_raw() -> ArcRwSignal { +fn use_params_raw() -> ArcMemo { use_context().expect( "Tried to access params outside the context of a matched .", ) @@ -165,9 +165,7 @@ fn use_params_raw() -> ArcRwSignal { /// Returns a raw key-value map of route params. #[track_caller] pub fn use_params_map() -> Memo { - // TODO this can be optimized in future to map over the signal, rather than cloning - let params = use_params_raw(); - Memo::new(move |_| params.get()) + use_params_raw().into() } /// Returns the current route params, parsed into the given type, or an error. diff --git a/router/src/nested_router.rs b/router/src/nested_router.rs index 8ef215973..a99f33870 100644 --- a/router/src/nested_router.rs +++ b/router/src/nested_router.rs @@ -569,15 +569,27 @@ where // the matched signal will also be updated on every match // it's used for relative route resolution let matched = ArcRwSignal::new(self.as_matched().to_string()); + let (parent_params, parent_matches): (Vec<_>, Vec<_>) = outlets + .iter() + .map(|route| (route.params.clone(), route.matched.clone())) + .unzip(); + let params_including_parents = { + let params = params.clone(); + ArcMemo::new({ + move |_| { + parent_params + .iter() + .flat_map(|params| params.get().into_iter()) + .chain(params.get()) + .collect::() + } + }) + }; let matched_including_parents = { - let parents = outlets - .iter() - .map(|route| route.matched.clone()) - .collect::>(); let matched = matched.clone(); ArcMemo::new({ move |_| { - parents + parent_matches .iter() .map(|matched| matched.get()) .chain(iter::once(matched.get())) @@ -616,12 +628,11 @@ where loaders.push(Box::pin(owner.with(|| { ScopedFuture::new({ let owner = outlet.owner.clone(); - let params = outlet.params.clone(); let url = outlet.url.clone(); let matched = Matched(matched_including_parents); let view_fn = Arc::clone(&outlet.view_fn); async move { - provide_context(params); + provide_context(params_including_parents); provide_context(url); provide_context(matched); view.preload().await; @@ -667,11 +678,11 @@ where outlets: &mut Vec>, parent: &Owner, ) { - let parent_matches = outlets + let (parent_params, parent_matches): (Vec<_>, Vec<_>) = outlets .iter() .take(*items) - .map(|route| route.matched.clone()) - .collect::>(); + .map(|route| (route.params.clone(), route.matched.clone())) + .unzip(); let current = outlets.get_mut(*items); match current { // if there's nothing currently in the routes at this point, build from here @@ -727,6 +738,18 @@ where } }) }; + let params_including_parents = { + let params = current.params.clone(); + ArcMemo::new({ + move |_| { + parent_params + .iter() + .flat_map(|params| params.get().into_iter()) + .chain(params.get()) + .collect::() + } + }) + }; // assign a new owner, so that contexts and signals owned by the previous route // in this outlet can be dropped @@ -742,11 +765,10 @@ where let owner = owner.clone(); let trigger = current.trigger.clone(); let url = current.url.clone(); - let params = current.params.clone(); let matched = Matched(matched_including_parents); let view_fn = Arc::clone(¤t.view_fn); async move { - provide_context(params); + provide_context(params_including_parents); provide_context(url); provide_context(matched); view.preload().await; diff --git a/router/src/params.rs b/router/src/params.rs index dea357601..e117cc1cd 100644 --- a/router/src/params.rs +++ b/router/src/params.rs @@ -2,8 +2,10 @@ use crate::location::{unescape, Url}; use std::{borrow::Cow, mem, str::FromStr, sync::Arc}; use thiserror::Error; +type ParamsMapInner = Vec<(Cow<'static, str>, String)>; + #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -pub struct ParamsMap(Vec<(Cow<'static, str>, String)>); +pub struct ParamsMap(ParamsMapInner); impl ParamsMap { /// Creates an empty map. @@ -88,6 +90,27 @@ where } } +impl IntoIterator for ParamsMap { + type Item = (Cow<'static, str>, String); + type IntoIter = ParamsMapIter; + + fn into_iter(self) -> Self::IntoIter { + ParamsMapIter(self.0.into_iter()) + } +} + +/// An iterator over the keys and values of a [`ParamsMap`]. +#[derive(Debug)] +pub struct ParamsMapIter(::IntoIter); + +impl Iterator for ParamsMapIter { + type Item = (Cow<'static, str>, String); + + fn next(&mut self) -> Option { + self.0.next() + } +} + /// A simple method of deserializing key-value data (like route params or URL search) /// into a concrete data type. `Self` should typically be a struct in which /// each field's type implements [`FromStr`].