Files
ujsx/tasks/reactiveroot-dispose.md
glm-5.1 c9c32a6aa6 decompose reconciler roadmap into 20 implementation tasks across 5 phases
Tasks follow the architecture spec phases:
- Phase 0: key field on UElement (2 tasks + review)
- Phase 1: reactive-host bridge / fiber tree (4 tasks + review)
- Phase 2: key-based children reconciliation (3 tasks + review)
- Phase 3: unmount & dispose support (4 tasks)
- Phase 4: TypeBox value optimizations (4 tasks)

Validated with taskgraph CLI: no cycles, 15 parallel generations,
3 high-risk tasks identified (signal-driven-updates, commit-mutations,
fiber-disposal).
2026-05-18 16:26:52 +00:00

2.5 KiB

id, name, status, depends_on, created, modified, scope, risk, impact, level
id name status depends_on created modified scope risk impact level
reactiveroot-dispose Implement ReactiveRoot.dispose() pending
review-reconciler
2026-05-18T16:22:57.292895316Z 2026-05-18T16:22:57.292895758Z narrow medium component implementation

Description

Implement real disposal in ReactiveRoot to close the signal subscription leak gap. Currently, ReactiveRoot has no dispose() method, subscribe() return values are not tracked, and render() overwrites the previous disposer without disposing it.

ReactiveRoot.dispose():

  1. Calls the render effect disposer and clears it
  2. Iterates all tracked subscriber disposers and calls each one
  3. Clears internal tracking state

subscribe() is updated to track disposer returns: each effect() created by subscribe() has its disposer stored in an internal list. The returned unsubscribe function both calls the disposer and removes it from tracking.

Both dispose() and the returned unsubscribe functions are idempotent.

Also implement real dispose on ReactiveNode — currently both reactiveComponent and reactiveElement return dispose: () => {}. With proper disposal tracking, these should return actual cleanup functions.

Acceptance Criteria

  • ReactiveRoot.dispose() method implemented
  • dispose() calls render effect disposer and clears renderDisposer
  • dispose() iterates all tracked subscriber disposers and calls each
  • dispose() clears internal tracking state after running
  • subscribe() tracks disposer returns in an internal Set<() => void>
  • subscribe() returned unsubscribe function calls disposer AND removes from tracking
  • dispose() and unsubscribe functions are idempotent (safe to call twice)
  • reactiveComponent and reactiveElement return real dispose functions (not no-ops)
  • ReactiveNode.dispose cleans up the underlying computed subscription
  • Existing tests pass
  • New test: ReactiveRoot.dispose() prevents future effect fires
  • New test: subscribe() unsubscribe removes from tracking
  • New test: double dispose() is safe
  • New test: reactiveComponent.dispose() prevents future computed evaluations

References

  • docs/architecture/reactive-layer.md — Known Gaps: all dispose functions are no-ops, subscribe return value thrown away, render overwrites previous disposer, no auto-dispose on unmount
  • docs/architecture/lifecycle.md — ReactiveRoot Disposal section

Notes

To be filled by implementation agent

Summary

To be filled on completion