DioxusLabs / dioxus

Fullstack app framework for web, desktop, mobile, and more.
https://dioxuslabs.com
Apache License 2.0
20.48k stars 788 forks source link

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

Open rambip opened 4 weeks ago

rambip commented 4 weeks ago

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 commented 4 weeks ago

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.

ealmloff commented 4 weeks ago

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: