Skip to content

[Medium][reactive] Composables create effects/computeds with no disposal path (memory leaks) #173

Description

@JosunLP

Severity: 🟡 Medium (memory leaks)

Several composables create long-lived reactive primitives with no disposal path, so they (and the timers/subscriptions they hold) leak whenever they are not created inside an active effectScope.

Locations & details

1. deferred() leaks its internal effect

src/concurrency/scheduling.ts:141-161deferred(source) creates an effect(() => …) subscribing to source and returns only readonly(mirror). There is no returned dispose and no onScopeDispose registration. Outside a scope, the effect (plus the scheduled timer/idle callback it manages) stays subscribed to source for the source's lifetime.

2. persistedSignal() persistence effect has no disposer

src/reactive/persisted.ts:63-71 — the persistence effect() returns no disposer. There is no way to stop persistence; and because effect() auto-registers with the ambient effectScope, calling persistedSignal() inside a scope means scope.stop() silently stops persistence while the signal keeps living.

3. Unscoped computed() retained by a long-lived signal

src/reactive/core.ts:23, src/reactive/computed.ts:137-142 — a Signal holds subscribers in a strong Set, and computed() only auto-disposes when created inside an active effectScope. A computed created at module/function scope, read once, depending on a long-lived signal, keeps its markDirty in the signal's subscriber set forever even after all user references drop.

Suggested fix

  • deferred(): return a dispose() (stop the effect + scheduled.cancel()), or register cleanup via onScopeDispose when a scope is active (mirroring usePolling/suspense).
  • persistedSignal(): isolate the persistence watcher from the ambient scope and/or return a dispose.
  • computed(): document the must-dispose contract prominently and/or provide auto-cleanup (FinalizationRegistry/WeakRef-based pruning), or have composables that internally create computeds aggregate their disposal.

Filed as part of a full-codebase security & correctness audit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions