mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
create separate URL/params signals for each route, to prevent updating them and running side effects while navigating away
This commit is contained in:
parent
15eeda9c7a
commit
2dd5efc5d0
3 changed files with 414 additions and 105 deletions
|
@ -260,27 +260,18 @@ where
|
|||
base
|
||||
});
|
||||
let routes = Routes::new(children.into_inner());
|
||||
let path = ArcMemo::new({
|
||||
let url = current_url.clone();
|
||||
move |_| url.read().path().to_string()
|
||||
});
|
||||
let search_params = ArcMemo::new({
|
||||
let url = current_url.clone();
|
||||
move |_| url.read().search_params().clone()
|
||||
});
|
||||
|
||||
let outer_owner =
|
||||
Owner::current().expect("creating Router, but no Owner was found");
|
||||
leptos::logging::log!("outer_owner = {:?}", outer_owner.debug_id());
|
||||
let params = ArcRwSignal::new(ParamsMap::new());
|
||||
|
||||
move || {
|
||||
path.track();
|
||||
current_url.track();
|
||||
FlatRoutesView {
|
||||
current_url: current_url.clone(),
|
||||
location: location.clone(),
|
||||
routes: routes.clone(),
|
||||
path: path.clone(),
|
||||
fallback: fallback(),
|
||||
outer_owner: outer_owner.clone(),
|
||||
params: params.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ use reactive_graph::{
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
future::Future,
|
||||
iter,
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
|
@ -42,14 +43,14 @@ use tachys::{
|
|||
};
|
||||
|
||||
pub(crate) struct FlatRoutesView<Loc, Defs, Fal, R> {
|
||||
pub current_url: ArcRwSignal<Url>,
|
||||
pub location: Option<Loc>,
|
||||
pub routes: Routes<Defs, R>,
|
||||
pub path: ArcMemo<String>,
|
||||
pub fallback: Fal,
|
||||
pub outer_owner: Owner,
|
||||
pub params: ArcRwSignal<ParamsMap>,
|
||||
}
|
||||
|
||||
/*
|
||||
impl<Loc, Defs, Fal, R> FlatRoutesView<Loc, Defs, Fal, R>
|
||||
where
|
||||
Loc: LocationProvider,
|
||||
|
@ -57,32 +58,65 @@ where
|
|||
Fal: Render<R>,
|
||||
R: Renderer + 'static,
|
||||
{
|
||||
pub async fn choose(
|
||||
pub fn choose(
|
||||
self,
|
||||
) -> Either<Fal, <Defs::Match as MatchInterface<R>>::View> {
|
||||
prev_owner: Option<&Owner>,
|
||||
prev_id: Option<RouteMatchId>,
|
||||
prev_params: Option<ArcRwSignal<ParamsMap>>
|
||||
) -> (
|
||||
Owner,
|
||||
Option<RouteMatchId>,
|
||||
ArcRwSignal<ParamsMap>,
|
||||
impl Future<Output = Either<Fal, <Defs::Match as MatchInterface<R>>::View>>,
|
||||
) {
|
||||
let FlatRoutesView {
|
||||
routes,
|
||||
path,
|
||||
fallback,
|
||||
outer_owner,
|
||||
params,
|
||||
..
|
||||
} = self;
|
||||
let new_match = routes.match_route(&path.read());
|
||||
let new_id = new_match.as_ref().map(|n| n.as_id());
|
||||
|
||||
outer_owner
|
||||
.with(|| {
|
||||
provide_context(params.clone().read_only());
|
||||
let new_match = routes.match_route(&path.read());
|
||||
match new_match {
|
||||
// update params or replace with new params signal
|
||||
// switching out the signal for a newly-created signal here means that navigating from,
|
||||
// for example, /foo/42 to /bar does not cause /foo/:id to respond to a change in `id`,
|
||||
// because the new set of params is set on a new signal
|
||||
let new_params = new_match
|
||||
.as_ref()
|
||||
.map(|matched| matched
|
||||
.to_params()
|
||||
.into_iter()
|
||||
.collect::<ParamsMap>()).unwrap_or_default();
|
||||
let new_params_signal = match prev_params {
|
||||
Some(prev_params) if prev_id == new_id => {
|
||||
prev_params.set(new_params);
|
||||
prev_params.clone()
|
||||
}
|
||||
_ => {
|
||||
let new_params_signal = ArcRwSignal::new(new_params);
|
||||
provide_context(ArcRwSignal::new(new_params_signal.clone()));
|
||||
new_params_signal
|
||||
}
|
||||
};
|
||||
|
||||
let owner = match prev_owner {
|
||||
Some(prev_owner) if prev_id == new_id => {
|
||||
prev_owner.clone()
|
||||
},
|
||||
_ => outer_owner.child()
|
||||
};
|
||||
|
||||
let (id, fut) = owner.with(|| {
|
||||
let id = new_match.as_ref().map(|n| n.as_id());
|
||||
(
|
||||
id,
|
||||
ScopedFuture::new(match new_match {
|
||||
None => EitherFuture::Left {
|
||||
inner: async move { fallback },
|
||||
},
|
||||
Some(matched) => {
|
||||
let new_params = matched
|
||||
.to_params()
|
||||
.into_iter()
|
||||
.collect::<ParamsMap>();
|
||||
params.set(new_params);
|
||||
let (view, child) = matched.into_view_and_child();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -94,12 +128,58 @@ where
|
|||
}
|
||||
|
||||
EitherFuture::Right {
|
||||
inner: ScopedFuture::new(view.choose()),
|
||||
inner: ScopedFuture::new({ let new_params_signal = new_params_signal.clone(); async move {
|
||||
provide_context(new_params_signal.clone());
|
||||
view.choose().await
|
||||
}}),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
}),
|
||||
)
|
||||
});
|
||||
(owner, id, new_params_signal, fut)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
pub struct FlatRoutesViewState<Defs, Fal, R>
|
||||
where
|
||||
Defs: MatchNestedRoutes<R> + 'static,
|
||||
Fal: Render<R> + 'static,
|
||||
R: Renderer + 'static
|
||||
{
|
||||
view: <EitherOf3<(), Fal, <Defs::Match as MatchInterface<R>>::View> as Render<R>>::State,
|
||||
id: Option<RouteMatchId>,
|
||||
owner: Owner,
|
||||
params: ArcRwSignal<ParamsMap>,
|
||||
path: String,
|
||||
url: ArcRwSignal<Url>
|
||||
}
|
||||
|
||||
impl<Defs, Fal, R> Mountable<R> for FlatRoutesViewState<Defs, Fal, R>
|
||||
where
|
||||
Defs: MatchNestedRoutes<R> + 'static,
|
||||
Fal: Render<R> + 'static,
|
||||
R: Renderer + 'static,
|
||||
{
|
||||
fn unmount(&mut self) {
|
||||
self.view.unmount();
|
||||
}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<R as Renderer>::Element,
|
||||
marker: Option<&<R as Renderer>::Node>,
|
||||
) {
|
||||
self.view.mount(parent, marker);
|
||||
}
|
||||
|
||||
fn insert_before_this(
|
||||
&self,
|
||||
parent: &<R as Renderer>::Element,
|
||||
child: &mut dyn Mountable<R>,
|
||||
) -> bool {
|
||||
self.view.insert_before_this(parent, child)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,65 +190,205 @@ where
|
|||
Fal: Render<R> + 'static,
|
||||
R: Renderer + 'static,
|
||||
{
|
||||
type State = Rc<
|
||||
RefCell<
|
||||
// TODO loading indicator
|
||||
<EitherOf3<(), Fal, <Defs::Match as MatchInterface<R>>::View> as Render<
|
||||
R,
|
||||
>>::State,
|
||||
>,
|
||||
>;
|
||||
type State = Rc<RefCell<FlatRoutesViewState<Defs, Fal, R>>>;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
let state = Rc::new(RefCell::new(EitherOf3::A(()).build()));
|
||||
let spawned_path = self.path.get_untracked();
|
||||
let current_path = self.path.clone();
|
||||
let location = self.location.clone();
|
||||
let route = self.choose();
|
||||
Executor::spawn_local({
|
||||
let state = Rc::clone(&state);
|
||||
async move {
|
||||
let loaded_route = route.await;
|
||||
// only update the route if it's still the current path
|
||||
// i.e., if we've navigated away before this has loaded, do nothing
|
||||
if &spawned_path == &*current_path.read_untracked() {
|
||||
let new_view = match loaded_route {
|
||||
Either::Left(i) => EitherOf3::B(i),
|
||||
Either::Right(i) => EitherOf3::C(i),
|
||||
};
|
||||
new_view.rebuild(&mut state.borrow_mut());
|
||||
if let Some(location) = location {
|
||||
location.ready_to_complete();
|
||||
let FlatRoutesView {
|
||||
current_url,
|
||||
location,
|
||||
routes,
|
||||
fallback,
|
||||
outer_owner,
|
||||
} = self;
|
||||
let current_url = current_url.read_untracked();
|
||||
|
||||
// we always need to match the new route
|
||||
let new_match = routes.match_route(¤t_url.path());
|
||||
let id = new_match.as_ref().map(|n| n.as_id());
|
||||
|
||||
// create default starting points for owner, url, path, and params
|
||||
// these will be held in state so that future navigations can update or replace them
|
||||
let owner = outer_owner.child();
|
||||
let url = ArcRwSignal::new(current_url.to_owned());
|
||||
let path = current_url.path().to_string();
|
||||
let params = ArcRwSignal::new(
|
||||
new_match
|
||||
.as_ref()
|
||||
.map(|n| n.to_params().into_iter().collect())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
match new_match {
|
||||
None => {
|
||||
Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: EitherOf3::B(fallback).build(),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
path,
|
||||
url
|
||||
}))
|
||||
}
|
||||
Some(matched) => {
|
||||
let (view, child) = matched.into_view_and_child();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if child.is_some() {
|
||||
panic!(
|
||||
"<FlatRoutes> should not be used with nested \
|
||||
routes."
|
||||
);
|
||||
}
|
||||
|
||||
let mut view = Box::pin(owner.with(|| ScopedFuture::new({
|
||||
let params = params.clone();
|
||||
let url = url.clone();
|
||||
async move {
|
||||
provide_context(params);
|
||||
provide_context(url);
|
||||
view.choose().await
|
||||
}
|
||||
})));
|
||||
|
||||
match view.as_mut().now_or_never() {
|
||||
Some(view) => {
|
||||
Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: EitherOf3::C(view).build(),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
path,
|
||||
url
|
||||
}))
|
||||
}
|
||||
None => {
|
||||
let state = Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: EitherOf3::A(()).build(),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
path,
|
||||
url,
|
||||
}));
|
||||
|
||||
Executor::spawn_local({
|
||||
let state = Rc::clone(&state);
|
||||
async move {
|
||||
let view = view.await;
|
||||
EitherOf3::C(view).rebuild(&mut state.borrow_mut().view);
|
||||
}
|
||||
});
|
||||
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
let spawned_path = self.path.get_untracked();
|
||||
let current_path = self.path.clone();
|
||||
let location = self.location.clone();
|
||||
let route = self.choose();
|
||||
Executor::spawn_local({
|
||||
let state = Rc::clone(&*state);
|
||||
async move {
|
||||
let loaded_route = route.await;
|
||||
// only update the route if it's still the current path
|
||||
// i.e., if we've navigated away before this has loaded, do nothing
|
||||
if &spawned_path == &*current_path.read_untracked() {
|
||||
let new_view = match loaded_route {
|
||||
Either::Left(i) => EitherOf3::B(i),
|
||||
Either::Right(i) => EitherOf3::C(i),
|
||||
};
|
||||
new_view.rebuild(&mut state.borrow_mut());
|
||||
if let Some(location) = location {
|
||||
location.ready_to_complete();
|
||||
}
|
||||
let FlatRoutesView {
|
||||
current_url,
|
||||
location,
|
||||
routes,
|
||||
fallback,
|
||||
outer_owner,
|
||||
} = self;
|
||||
let owner = outer_owner.child();
|
||||
let url_snapshot = current_url.get_untracked();
|
||||
|
||||
// if the path is the same, we do not need to re-route
|
||||
// we can just update the search query and go about our day
|
||||
let mut initial_state = state.borrow_mut();
|
||||
if url_snapshot.path() == initial_state.path {
|
||||
initial_state.url.set(url_snapshot.to_owned());
|
||||
return;
|
||||
}
|
||||
|
||||
// since the path didn't match, we'll update the retained path for future diffing
|
||||
initial_state.path.clear();
|
||||
initial_state.path.push_str(url_snapshot.path());
|
||||
|
||||
// otherwise, match the new route
|
||||
let new_match = routes.match_route(url_snapshot.path());
|
||||
let new_id = new_match.as_ref().map(|n| n.as_id());
|
||||
let matched_params =
|
||||
new_match
|
||||
.as_ref()
|
||||
.map(|n| n.to_params().into_iter().collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
// if it's the same route, we just update the params
|
||||
if new_id == initial_state.id {
|
||||
initial_state.params.set(matched_params);
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, we need to update the retained path for diffing
|
||||
initial_state.id = new_id;
|
||||
|
||||
// otherwise, it's a new route, so we'll need to
|
||||
// 1) create a new owner, URL signal, and params signal
|
||||
// 2) render the fallback or new route
|
||||
let owner = outer_owner.child();
|
||||
let url = ArcRwSignal::new(url_snapshot.to_owned());
|
||||
let params = ArcRwSignal::new(matched_params);
|
||||
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 = mem::replace(&mut initial_state.params, params.clone());
|
||||
|
||||
// we drop the route state here, in case there is a <Redirect/> or similar that occurs
|
||||
// while rendering either the fallback or the new route
|
||||
drop(initial_state);
|
||||
|
||||
match new_match {
|
||||
// render fallback
|
||||
None => {
|
||||
owner.with(|| {
|
||||
provide_context(url);
|
||||
provide_context(params);
|
||||
EitherOf3::B(fallback).rebuild(&mut state.borrow_mut().view)
|
||||
});
|
||||
},
|
||||
Some(matched) => {
|
||||
let (view, child) = matched.into_view_and_child();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if child.is_some() {
|
||||
panic!(
|
||||
"<FlatRoutes> should not be used with nested \
|
||||
routes."
|
||||
);
|
||||
}
|
||||
|
||||
let spawned_path = url_snapshot.path().to_string();
|
||||
|
||||
Executor::spawn_local(owner.with(|| ScopedFuture::new({
|
||||
let state = Rc::clone(&state);
|
||||
async move {
|
||||
provide_context(url);
|
||||
provide_context(params);
|
||||
// TODO if we want, we could resolve() this here to wait for data to load
|
||||
let view = view.choose().await;
|
||||
|
||||
// only update the route if it's still the current path
|
||||
// i.e., if we've navigated away before this has loaded, do nothing
|
||||
if current_url.read_untracked().path() == spawned_path {
|
||||
EitherOf3::C(view).rebuild(&mut state.borrow_mut().view);
|
||||
}
|
||||
|
||||
if let Some(location) = location {
|
||||
location.ready_to_complete();
|
||||
}
|
||||
|
||||
drop(old_owner);
|
||||
drop(old_params);
|
||||
drop(old_url);
|
||||
}
|
||||
})));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,6 +413,42 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
impl<Loc, Defs, Fal, R> FlatRoutesView<Loc, Defs, Fal, R>
|
||||
where
|
||||
Loc: LocationProvider + Send,
|
||||
Defs: MatchNestedRoutes<R> + Send + 'static,
|
||||
Fal: RenderHtml<R> + 'static,
|
||||
R: Renderer + 'static,
|
||||
{
|
||||
fn choose_ssr(self) -> Either<Fal, <Defs::Match as MatchInterface<R>>::View> {
|
||||
let current_url = self.current_url.read_untracked();
|
||||
let new_match = self.routes.match_route(¤t_url.path());
|
||||
let owner = self.outer_owner.child();
|
||||
let url = ArcRwSignal::new(current_url.to_owned());
|
||||
let params = ArcRwSignal::new(
|
||||
new_match
|
||||
.as_ref()
|
||||
.map(|n| n.to_params().into_iter().collect::<ParamsMap>())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
match new_match {
|
||||
None => Either::Left(self.fallback),
|
||||
Some(matched) => {
|
||||
let (view, _) = matched.into_view_and_child();
|
||||
let view = owner.with(|| ScopedFuture::new(async move {
|
||||
provide_context(url);
|
||||
provide_context(params);
|
||||
view.choose().await
|
||||
}))
|
||||
.now_or_never()
|
||||
.expect("async route used in SSR");
|
||||
Either::Right(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Loc, Defs, Fal, R> RenderHtml<R> for FlatRoutesView<Loc, Defs, Fal, R>
|
||||
where
|
||||
Loc: LocationProvider + Send,
|
||||
|
@ -258,10 +514,7 @@ where
|
|||
|
||||
RouteList::register(RouteList::from(routes));
|
||||
} else {
|
||||
let route = Box::pin(self.choose());
|
||||
route
|
||||
.now_or_never()
|
||||
.expect("async route used in SSR")
|
||||
self.choose_ssr()
|
||||
.to_html_with_buf(buf, position);
|
||||
}
|
||||
}
|
||||
|
@ -273,10 +526,7 @@ where
|
|||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
let route = Box::pin(self.choose());
|
||||
route
|
||||
.now_or_never()
|
||||
.expect("async route used in SSR")
|
||||
self.choose_ssr()
|
||||
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position);
|
||||
}
|
||||
|
||||
|
@ -285,21 +535,86 @@ where
|
|||
cursor: &Cursor<R>,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
let spawned_path = self.path.get_untracked();
|
||||
let current_path = self.path.clone();
|
||||
let location = self.location.clone();
|
||||
let route = Box::pin(self.choose());
|
||||
match route.now_or_never() {
|
||||
// this can be mostly the same as the build() implementation, but with hydrate()
|
||||
//
|
||||
// however, the big TODO is that we need to support lazy hydration in the case that the
|
||||
// route is lazy-loaded on the client -- in this case, we actually can't initially hydrate
|
||||
// at all, but need to skip, because the HTML will contain the route even though the
|
||||
// client-side route component code is not yet loaded
|
||||
let FlatRoutesView {
|
||||
current_url,
|
||||
location,
|
||||
routes,
|
||||
fallback,
|
||||
outer_owner,
|
||||
} = self;
|
||||
let current_url = current_url.read_untracked();
|
||||
|
||||
// we always need to match the new route
|
||||
let new_match = routes.match_route(¤t_url.path());
|
||||
let id = new_match.as_ref().map(|n| n.as_id());
|
||||
|
||||
// create default starting points for owner, url, path, and params
|
||||
// these will be held in state so that future navigations can update or replace them
|
||||
let owner = outer_owner.child();
|
||||
let url = ArcRwSignal::new(current_url.to_owned());
|
||||
let path = current_url.path().to_string();
|
||||
let params = ArcRwSignal::new(
|
||||
new_match
|
||||
.as_ref()
|
||||
.map(|n| n.to_params().into_iter().collect())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
match new_match {
|
||||
None => {
|
||||
todo!()
|
||||
Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: EitherOf3::B(fallback).hydrate::<FROM_SERVER>(cursor, position),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
path,
|
||||
url
|
||||
}))
|
||||
}
|
||||
Some(matched) => Rc::new(RefCell::new(
|
||||
match matched {
|
||||
Either::Left(inner) => EitherOf3::B(inner),
|
||||
Either::Right(inner) => EitherOf3::C(inner),
|
||||
Some(matched) => {
|
||||
let (view, child) = matched.into_view_and_child();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if child.is_some() {
|
||||
panic!(
|
||||
"<FlatRoutes> should not be used with nested \
|
||||
routes."
|
||||
);
|
||||
}
|
||||
.hydrate::<FROM_SERVER>(cursor, position),
|
||||
)),
|
||||
|
||||
let mut view = Box::pin(owner.with(|| ScopedFuture::new({
|
||||
let params = params.clone();
|
||||
let url = url.clone();
|
||||
async move {
|
||||
provide_context(params);
|
||||
provide_context(url);
|
||||
view.choose().await
|
||||
}
|
||||
})));
|
||||
|
||||
match view.as_mut().now_or_never() {
|
||||
Some(view) => {
|
||||
Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: EitherOf3::C(view).hydrate::<FROM_SERVER>(cursor, position),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
path,
|
||||
url
|
||||
}))
|
||||
}
|
||||
None => {
|
||||
// see comment at the top of this function
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@ pub fn use_location() -> Location {
|
|||
}
|
||||
|
||||
#[track_caller]
|
||||
fn use_params_raw() -> ArcReadSignal<ParamsMap> {
|
||||
fn use_params_raw() -> ArcRwSignal<ParamsMap> {
|
||||
use_context().expect(
|
||||
"Tried to access params outside the context of a matched <Route>.",
|
||||
)
|
||||
|
@ -157,9 +157,12 @@ where
|
|||
|
||||
#[track_caller]
|
||||
fn use_url_raw() -> ArcRwSignal<Url> {
|
||||
let RouterContext { current_url, .. } = use_context()
|
||||
.expect("Tried to access reactive URL outside a <Router> component.");
|
||||
current_url
|
||||
use_context().unwrap_or_else(|| {
|
||||
let RouterContext { current_url, .. } = use_context().expect(
|
||||
"Tried to access reactive URL outside a <Router> component.",
|
||||
);
|
||||
current_url
|
||||
})
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
|
Loading…
Reference in a new issue