dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

`use_branched_signal` Hook to convert from `ReadOnlySignal` to `Signal`

Open rambip opened this issue 1 year ago • 2 comments

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)

rambip avatar Sep 05 '24 18:09 rambip

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.

rambip avatar Sep 05 '24 20:09 rambip

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)

ealmloff avatar Sep 06 '24 12:09 ealmloff