Skip to content

ReadSignal coercion can cause extra reruns #5474

@ealmloff

Description

@ealmloff

Problem

When read signal swaps in place, it transfers more subscribers than it should which can cause scopes that subscribe to the old signal before coercion to rerun when the new signal changes

Steps To Reproduce

Run this example and follow the steps in the description:

#![allow(non_snake_case)]

use dioxus::prelude::*;

fn main() {
    dioxus::launch(app);
}

fn app() -> Element {
    let mut use_b = use_signal(|| false);
    let mut effect_runs = use_signal(|| 0);
    let mut signal_a = use_signal(|| 0);
    let mut signal_b = use_signal(|| 0);

    use_effect(move || {
        let value = signal_a();
        let runs = {
            let mut write = effect_runs.write();
            *write += 1;
            *write
        };
        println!("parent effect ran: A = {value}, runs = {runs}");
    });

    let child_signal = if use_b() { signal_b } else { signal_a };

    rsx! {
        div {
            max_width: "52rem",
            margin: "0 auto",
            padding: "2rem",
            font_family: "sans-serif",

            h1 { "Mapped ReadSignal point_to repro" }
            p {
                "Steps: leave A and B equal, swap the child from A to B, then increment B. "
                "The parent effect below only reads A, so its run count should not change when B changes."
            }

            div {
                display: "flex",
                gap: "0.75rem",
                flex_wrap: "wrap",
                margin_bottom: "1rem",

                button {
                    onclick: move |_| use_b.set(false),
                    "Child uses A"
                }
                button {
                    onclick: move |_| use_b.set(true),
                    "Child uses B"
                }
                button {
                    onclick: move |_| signal_a += 1,
                    "Increment A"
                }
                button {
                    onclick: move |_| signal_b += 1,
                    "Increment B"
                }
                button {
                    onclick: move |_| {
                        signal_a.set(0);
                        signal_b.set(0);
                        use_b.set(false);
                        effect_runs.set(0);
                    },
                    "Reset"
                }
            }

            ul {
                li { "Parent effect runs: {effect_runs}" }
                li { "A: {signal_a}" }
                li { "B: {signal_b}" }
                li {
                    "Child source: "
                    if use_b() { "B" } else { "A" }
                }
            }

            Child { sig: child_signal }
        }
    }
}

#[component]
fn Child(sig: ReadSignal<i32>) -> Element {
    rsx! {
        div {
            margin_top: "1rem",
            padding: "1rem",
            border: "1px solid #ccc",
            border_radius: "8px",
            "Child value: {sig}"
        }
    }
}

Expected behavior

The effect should only rerun when signal_a changes. I think we can fix this issue by keeping a separate layer of subscribers for readsignal and only moving those subscribers instead of the whole list

Environment:

  • Dioxus version: main
  • Rust version: nightly
  • OS info: macos
  • App platform: all

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsignalsRelated to the signals crate

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions