dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

use_effect + signals = footgun

Open werner291 opened this issue 1 year ago • 1 comments

Hi,

the following code doesn't work.

pub fn App(cx: Scope) -> Element {
    let foo = use_signal(cx, || 0);

    println!("Rendering App {:?}", foo.read());

    use_effect(cx, &(foo, ), |(foo, )| async move {
        println!("Effect: {:?}", foo);
    });

    use_on_create(cx, || async move {
        sleep(std::time::Duration::from_secs(1)).await;

        foo.set(1);
    });

    render! {
        "Hello {foo}"
    }
}

I don't know terribly much about how memoization works exactly... but what I think happened with my limited understanding:

  • The hook runs the first time, and caches the dependency. (Which is the signal itself.)
  • The signal's value is changed; this also updates the value of the signal held by the hook.
  • The second time the hook runs, the signal is compared with the one held in the hook... But that one got updated in the background, so we basically the hook checks the signal's current value with the current value, and the effect doesn't fire.

This is... a bit of a footgun, I think. Would it make sense to introduce a marker trait for dependencies, rather than just using a blanket implementation for anything that is Clone+PartialEq?

werner291 avatar Dec 31 '23 16:12 werner291

Signals are not compatible with all hooks in dioxus-hooks currently. The dioxus-signals crate defines a different use_effect hook for signals.

Signals track reads instead of memorization which means you don't need to add them to the dependency tuple, but until signals are integrated into dioxus-hooks, you will need to either:

  1. use the dioxus-signals version of use_effect
pub fn App(cx: Scope) -> Element {
    let foo = use_signal(cx, || 0);

    println!("Rendering App {:?}", foo.read());

    dioxus_signals::use_effect(cx, move || {
        println!("Effect: {:?}", foo);
    });

    use_on_create(cx, || async move {
        sleep(std::time::Duration::from_secs(1)).await;

        foo.set(1);
    });

    render! {
        "Hello {foo}"
    }
}
  1. Or add the read cloned value as a dependancy
pub fn App(cx: Scope) -> Element {
    let foo = use_signal(cx, || 0);

    println!("Rendering App {:?}", foo.read());

    use_effect(cx, &(foo.read().clone(), ), |(foo, )| async move {
        println!("Effect: {:?}", foo);
    });

    use_on_create(cx, || async move {
        sleep(std::time::Duration::from_secs(1)).await;

        foo.set(1);
    });

    render! {
        "Hello {foo}"
    }
}

In https://github.com/DioxusLabs/dioxus/pull/1676, any time a signal reads inside an effect, it should add itself as a dependency of that effect which would your original code or this code work:

pub fn App(cx: Scope) -> Element {
    let foo = use_signal(cx, || 0);

    println!("Rendering App {:?}", foo.read());

    use_effect!(cx, || async move {
        println!("Effect: {:?}", foo);
    });

    use_on_create(cx, || async move {
        sleep(std::time::Duration::from_secs(1)).await;

        foo.set(1);
    });

    render! {
        "Hello {foo}"
    }
}

ealmloff avatar Dec 31 '23 17:12 ealmloff

Signals are now the default for effects which fixes this issue

ealmloff avatar Mar 12 '24 19:03 ealmloff