Skip to content

fix(store): release unobserved computed atoms with retained dependency links - #373

Draft
Sheraff wants to merge 2 commits into
TanStack:mainfrom
Sheraff:codex/fix-unobserved-atom-retention
Draft

Sheraff wants to merge 2 commits into
TanStack:mainfrom
Sheraff:codex/fix-unobserved-atom-retention

Conversation

@Sheraff

@Sheraff Sheraff commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Warning

this is fully ai authored, and it adds +250 gzip bytes

🎯 Changes

Reading a computed atom with .get() currently leaves references from its sources, keeping the computed alive even when it has no subscribers. This change detaches those subscriber links while retaining the computed's dependency links for cache validation and reuse. Subscribing reconnects the retained links, preserving unchanged cached snapshots.

Adds regression coverage for dependency chains, conditional dependencies, custom comparisons, subscription transitions, getter failures, reentrant writes, and async completion, plus a patch changeset.

Fixes #372.

Further performance optimization is deferred until benchmark coverage is available; this PR contains no benchmark infrastructure or reactive-engine rewrite.

How dependency tracking changes

Both directions of tracking already exist on main. For derived = createAtom(() => source.get() + 1), evaluating the getter creates a single Link object that belongs to two lists:

  • source.subs lists the computations that depend on source, so writes can notify them.
  • derived.deps lists the sources that derived depends on, so its dependencies can be tracked and cleaned up.

These are two views of the same link, not two different kinds of link introduced by this PR:

source.subs  → Link → derived
derived.deps → Link → source
               ↑
        the same Link object

Today, a bare derived.get() leaves that link in both lists. As long as source is reachable, source.subs keeps the link—and therefore derived—reachable too. Dropping the application's reference to derived is not enough to collect it.

The subscription/unsubscription workaround removes the link from both lists when the last subscriber leaves. That releases derived, but also discards its dependency information and marks it dirty.

This PR separates removing the reverse reference from discarding the dependency information:

Lifecycle event On main With this PR
After an unobserved get() Link remains in both lists Link remains only in derived.deps
After the last unsubscribe Link is removed from both lists Link remains only in derived.deps
A later subscription Discarded dependencies must be tracked again Retained links can be validated and reconnected

The computed can still reach its sources, but its sources no longer keep it alive. If application code retains derived, later reads can validate its cached dependencies. If application code drops it, the garbage collector can collect it and its retained links. No FinalizationRegistry is needed: detachment happens when there are no subscribers, rather than waiting to detect that the computed has become unreachable.

Validation

  • pnpm test:pr: 299 runtime tests passed, one skipped. Type checks, lint, dependency checks, library builds, and package validation passed.
  • The full command remains unsuccessful locally because five Angular example builds fail (including esbuild deadlock errors). Retrying those builds serially also failed on Node 25.8.1. Nx Cloud was unreachable locally.
  • Manual garbage-collection check: none of 100 discarded read-only computed atoms remained retained; subscriptions still work when their handles are discarded.
  • Changed files pass Prettier and git diff --check.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr (results and limitations above).

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

@coderabbitai

coderabbitai Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit c330408

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 2m 15s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-09-28 21:31:01 UTC

@pkg-pr-new

pkg-pr-new Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
@tanstack/angular-store

npm i https://pkg.pr.new/@tanstack/angular-store@373

@tanstack/lit-store

npm i https://pkg.pr.new/@tanstack/lit-store@373

@tanstack/octane-store

npm i https://pkg.pr.new/@tanstack/octane-store@373

@tanstack/preact-store

npm i https://pkg.pr.new/@tanstack/preact-store@373

@tanstack/react-store

npm i https://pkg.pr.new/@tanstack/react-store@373

@tanstack/solid-store

npm i https://pkg.pr.new/@tanstack/solid-store@373

@tanstack/store

npm i https://pkg.pr.new/@tanstack/store@373

@tanstack/svelte-store

npm i https://pkg.pr.new/@tanstack/svelte-store@373

@tanstack/vue-store

npm i https://pkg.pr.new/@tanstack/vue-store@373

commit: a7cc518

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reading a computed atom without subscribing can prevent garbage collection

1 participant