Add State To Navigation
Feature Request
State should be able to be preserved through navigation. E.g. api
- Go to new route with the provided state
let state = ...;
use_navigator().push_with_state(Route::Index {}, state);
- Set state for current route.
let state = ...;
use_navigator().set_state(state);
// E.g. Then can move to new route
// use_navigator().push(Route::Index {});
- Get the state for the current route
let state = use_navigator().get_state();
- Go to the previous route with preserved state
use_navigator().go_back_with_state();
- Drop state
use_navigator().clear_state();
Implementation
Use the history api, but don't store the state in the history api. Instead keep the state in an in-memory state store and have a history api hook that adds and pops states. Serde would not be needed in this case. Any type Clone + 'static could be accepted in set_state, and an Option<Box<dyn Clone + 'static>> is returned from get_state.
Additional Considerations
It may be performant to switch to Clone + Send + Sync + 'static as input. Then output could return Arc<RwLock<Option<Box<dyn Clone + Send + Sync + 'static>>>>, which would avoid unneeded clones.
If we decide to go the Option<Box<dyn Clone + 'static>> route. It may be wise to add these methods as well that do not require a clone.
- Mutate state without clone (Will panic if
with_state_mutorwith_stateis called again before closure finishes)
let value = use_navigator().with_state_mut(|state| { 1 });
- Read state without clone (Will panic if
with_state_mutcalled again before closure finishes)
let value = use_navigator().with_state(|state| { 1 });
In either Arc<RwLock<Option<Box<dyn Clone + Send + Sync + 'static>>>> or Option<Box<dyn Clone + 'static>> we should likely actually return a wrapper struct with convenience methods.
The initial version of the type-safe router supported fields that were not present in the URL as the navigation state under a feature flag. For example, navigation_state_1 and navigation_state_2 used to be stored in the navigation state instead of the URL in this route:
use dioxus::prelude::*;
fn App() -> Element {
rsx! {
Router::<Route> { }
}
}
// Routes are generally enums that derive `Routable`
#[derive(Routable, Clone, PartialEq, Debug, Serialize, Deserialize)]
enum Route {
// Each enum has an associated url
#[route("/")]
Home { navigation_state_1: u64, navigation_state_2: u64 },
}
If they were not found, then a default value was used. This was removed because it required the additional bound of serialize + deserialize on the routable trait which made the feature flag non-addative and made the code more difficult to maintain.
We could restore the feature, but instead of adding the bound to the trait, we could expose a way to get and modify the current navigation state in the routable trait. Then the feature could still be additive and your navigation state remains typesafe
Comments On Previous Implementation
If they were not found, then a default value was used.
This should also require the Default bound, correct?
This was removed because it required the additional bound of serialize + deserialize on the routable trait which made the feature flag non-additive
If we made all routes required to also explicitly be Serialize, Deserialize, or have dioxus implement these in Routable and re-export serde, then it no longer would be non-additive. Which may be reasonable for any route.
Comments On New Suggested Implemention
We could restore the feature, but instead of adding the bound to the trait, we could expose a way to get and modify the current navigation state in the routable trait. Then the feature could still be additive and your navigation state remains typesafe
A type safe state per route is ideal. Though maybe an explicit #[state] attribute would be appropriate for state fields, instead of being implicit. There should probably be only one state field, as having more than would would make some apis impossible (see examples below, some are not possible if there are multiple states). State should implement Default.
I'm not sure how useful it would be to have the ability to get these fields optional come from the url path (like in the previous implementation). Sounds likely to introduce bugs if used this way.
TLDR; There should only be one state field annotated with #[state] that will be assumed to implement Default
puesdo code e.g.
#[derive(Routable, Clone, PartialEq, Debug)]
enum Route {
#[route("/")]
Home {
#[state]
state: StateType1
},
#[route("/page")]
Page {
#[state]
state: StateType2
},
}
usage with current api:
use_navigator().push(Route::Page { state });
usage with new api:
let state: Signal<StateType2> = use_navigator().get_state().expect("State for the current route should be type StateType2");
// Replaces the current state with `::default()` and returns the current state. Useful to be paired with a `.push(..)` when saving is not needed and cloning is trying to be avoided for large states
let state: StateType2 = use_navigator().take_state().expect("State for the current route should be type StateType2 and a state signal from `get_state` should not still exist");
// Go back to previous route, but overwrite state with provided state. Will fail silently if there is no previous location to go to or the previous state types do not match
use_navigator().go_back_with_state(state);
// Go back to forward route, but overwrite state with provided state. Will fail silently if there is no forward location to go to or the forward state types do not match
use_navigator().go_forward_with_state(state);
One drawback of this state per route approach is that moving states between routes is slightly tedious if you want to avoid cloning large states.
// forward
let state: StateType2 = use_navigator().take_state().unwrap();
use_navigator().push(Route::Page { state });
// use_navigator().go_forward_with_state(state); // or
// back
let state: StateType2 = use_navigator().take_state().unwrap();
use_navigator().go_back_with_state(state);