`use_branched_signal` Hook to convert from `ReadOnlySignal` to `Signal`
Feature Request
Let's say I want to build a web app where the state is defined this way:
enum State {
HomeScreen,
MyApp{favorite_food: String}
}
Initially, the state is HomeScreen, but after the user go through the initial setup, the state is App{favorite_food: "pasta"} (let's say the favorite_food comes from a http request). I then want to change the initial "favorite_food" guess if the user changes this information. Conceptually, the code would look like this:
enum State {
HomeScreen,
App {favorite_food: String}
}
#[component]
fn HomeScreen() -> Element {
todo!()
}
#[component]
fn App(initial_favorite_food: ReadOnlySignal<String>) -> Element {
let mut favorite_food: Signal<String> = todo!();
todo!()
}
#[component]
fn MyApp() -> Element {
let state = use_signal(|| State::HomeScreen);
rsx! {
button {
onclick: move |_| state.set(State::App { favorite_food: "pasta".to_string() })
}
match state.read() {
State::HomeScreen => rsx!{ HomeScreen {} },
State::App{favorite_food: food} => rsx! {App { initial_favorite_food: food }
}
}
}
}
The issue is that I don't know how to create a new Signal from a ReadOnlySignal. This new signal should update both when initial_relative_food is updated and when the component App changes it (interior mutability)
Non-solutions
provide initial_favorite_food as a value instead of a signal
It works, but if the state signal is updated with a new value, it will not update the component.
This solution also won't work for a non-Clone value.
use use_effect
As said in the documentation, "Effects are reactive closures that run after the component has finished rendering". It should not be used to derive state, but I want to derive state in this case.
use use_memo
This would allow to do some computation to initial_favorite_food and react to changes, but no interior mutability.
Implement Suggestion
I think I found a nice way to implement the behaviour I want:
fn use_branched_signal<T: 'static>(mut f: impl FnMut() -> T + 'static) -> Signal<T> {
let location = std::panic::Location::caller();
use_hook(|| {
let (rc, mut changed) = ReactiveContext::new_with_origin(location);
let value = rc.reset_and_run_in(&mut f);
let mut result = Signal::new(value);
spawn_isomorphic(async move {
while changed.next().await.is_some() {
// Remove any pending updates
while changed.try_next().is_ok() {}
let new_value = rc.run_in(&mut f);
result.set(new_value)
}
});
result
})
}
To use it, simply do
#[component]
fn App(initial_favorite_food: ReadOnlySignal<String>) -> Element {
let mut favorite_food: Signal<String> = use_branched_signal(|| initial_favorite_food());
todo!()
}
Other uses
In essence, my proposition is just a new hook that is identical to use_signal, but in addition it update when a dependency in the closure changes. I think it would allow a lot of other nice patterns that would otherwise be impossible (or require optional values with unwraps)
As proposed by @ealmloff , a possible way to solve this issue is to add a way to automatically reset the component when an input changes.
This was discussed some more on the discord. use_branched_signal looses information when you update the signal manually. It is a bit too complex for dioxus to adopt in the core hooks package
Dioxus does let you reset a component by putting it in an iterator with a key that depends on the props (or hash of the props). React published an article about a very similar issue a while ago: https://legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key
{std::iter::once(
rsx! { Explorer { key: "{positions:?}-{time_window:?}", link_stream: stream, initial_positions: positions, initial_time_windw: time_window } }
)}
We could provide a wrapper over the iterator approach with a more clear name:
Reset {
when_value_changes: (positions, time_window),
Explorer { link_stream: stream, initial_positions: positions, initial_time_windw: time_window }
}
Related issues/discussions:
- https://github.com/DioxusLabs/dioxus/discussions/2864
- I have also seen this issue a lot with the router. Some people expect the component for the route to be recreated when you change the props (for example going from
/user/123->/user/1234)