leptos-rs / leptos

Build fast web applications with Rust.
https://leptos.dev
MIT License
15.31k stars 599 forks source link

Effects/Memos should't be able to "own" a signal #2560

Closed tqwewe closed 2 months ago

tqwewe commented 2 months ago

Describe the bug When creating a signal from within an effect or memo, the signal will be owned by that scope and will be disposed of after the effect/memo is ran.

Is there any reason or use case why an effect/memo might want to actually own a signal? Would it make more sense for the runtime to automatically climb up the scopes to determine the closest component and use that as the owner instead?

To Reproduce

  1. Create a signal in the component let root: RwSignal<Vec<RwSignal<i32>>> = RwSignal::new(Vec::new());
  2. In an effect, push a new signal to the root signal.
    Effect::new(move |_| {
       update!(|root| root.push(RwSignal::new(1)));
    });
  3. Try to write to the inner signal and see an error saying the inner signal has already been disposed.

To fix this, you need to wrap the inner signal in a leptos::with_owner, referencing the outer component as the owner.

Expected behavior There should be no need to manually point the runtime to the correct owner, and the nearest component should be inferred.

Additional context I don't know the inner workings of Leptos well enough, so it might not even be the case that it can possibly crawl up some kind of tree of owners to find the nearest component.

gbj commented 2 months ago

If you haven't already, reading The Life Cycle of a Signal at the end of the book may be helpful in answering why effects/memos own signals.

The short version is that components don't exist at runtime. A "component" is just a function that is called to set up the DOM and reactive system. Creating a signal does climb up the scopes to find the nearest owner—it's just that owners and components are not the same thing.

An example like the one you provided is one of the reasons I'd generally say to avoid writing to signals from effects, but rather to derive state from other state. There's a relatively straightforward escape hatch, as you note.

tqwewe commented 2 months ago

Ah okay makes sense thank you.

I always try to brainstorm how to avoid using effects in Leptos, but in my particular use case I couldn't think of a better one.

I'm using leptos_use's use_event_source for sever sent events. This returns a Signal<Option<T>> containing the latest event in the event source.

I want to store an accumulated list of the events in my app, and so decided to go with a RwSignal<Vec<...>>. However the only way I could think of to accumulate values in this list was to use an effect to push the last event to this list.

let events = RwSignal::new(Vec::new());

let UseEventSourceReturn { data, .. } = use_event_source("https://event-source-url");
Effect::new(move |_| {
    if let Some(event) = data.get() {
        update!(|events| events.push(event));
    }
});

One solution I tried was to use a Memo, which uses its own return value, similar to a fold. But if I want to remove events older than 5 minutes in a set_interval for example, then I cannot really update the memo.

Is effect really required in my case? Is it the fault of leptos_use's event source API, not providing somekind of callback method for me to use? Or is there a much more obvious solution I'm missing.

gbj commented 2 months ago

One plausible way to do the kind of fold you're talking about: (This is just pseudo-code)

let interval = /* some signal that updates every 5 minutes */;
let UseEventSourceReturn { data, .. } = use_event_source("https://event-source-url");
let events = create_owning_memo(|prev: Option<Vec<T>>| {
  let most_recent_event = data.get();
  interval.track(); // make sure this runs at least every five minutes
  let mut events = prev.unwrap_or_default();
  events.retain(/* filter out the events older than 5 minutes */);
  if events.last() != Some(most_recent_event) {
    events.push(most_recent_event);
  }
  (events, true)
});

I'd say an effect is never really required unless you are synchronizing reactive state with the non-reactive world and so need to read the "leaves" of the reactive graph/tree. But YMMV as to which is easier to use.

tqwewe commented 2 months ago

Much appreciated, will try to implement the interval as a signal approach in the memo as you've suggested